ClawMem — On-device memory layer for Claude Code, OpenClaw, and Hermes agents
Memory server
First committed by yoloshii
Published in yoloshii/ClawMem. Publisher page
212 GitHub stars
What it is
On-device memory layer for AI agents. Claude Code, OpenClaw, and Hermes. Hooks + MCP server + hybrid RAG search.
AutoGineer overview, from the brain’s own files
ClawMem is a local-first memory system designed to provide persistent, context-aware memory for AI agents like Claude Code, OpenClaw, and Hermes. It operates entirely on-device without cloud dependencies or API keys, using a hybrid architecture that combines BM25, vector search, and multi-graph traversal. The system automatically captures session decisions, preferences, and milestones, while providing an MCP server and hooks to surface relevant context during agent interactions. It features sophisticated memory management, including contradiction detection, causal relationship inference, and document lifecycle policies, ensuring agents maintain continuity across sessions.
How it works
Memory model: Hybrid
AutoGineer overview, from the brain’s own files
ClawMem functions as a local SQLite-backed memory vault. It uses hooks to automatically inject relevant context into agent prompts and captures session data for future use. The system employs a hybrid retrieval model that includes BM25, vector search, and graph traversal to surface information. It processes data through an enrichment pipeline that extracts entities, links facts, and evolves memory metadata. Background workers handle maintenance tasks like consolidation and deductive synthesis, while the MCP server allows agents to perform targeted queries and retrieve specific information when needed.
| Folder | Role | Files |
|---|---|---|
| .github | agent-config | 5 |
| agents | agent-instructions | 1 |
| bin | scripts | 1 |
| docs | docs | 30 |
| scripts | scripts | 14 |
| src | source-code | 121 |
| tests | tests | 211 |
| eval-bundles | folder | 4 |
| extras | folder | 7 |
The agent reads
- query_plan evidence: AGENTS.md
- find_similar evidence: AGENTS.md
- find_causal_links evidence: AGENTS.md
- SKILL.md evidence: AGENTS.md
- CLAUDE.md evidence: CLAUDE.md
- AGENTS.md evidence: CLAUDE.md
The agent writes
- src/indexer.ts evidence: AGENTS.md
Requirements
Runtimes: Node.js, Bun
Databases: LanceDB, SQLite
API keys (by name): ANTHROPIC_API_KEY, CLAWMEM_API_TOKEN, CLAWMEM_EMBED_API_KEY, CLAWMEM_JUDGE_API_KEY, CLAWMEM_LLM_API_KEY, CLAWMEM_RERANK_API_KEY
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Google Antigravity, Kiro, OpenCode, Amp, Roo Code, Cline, JetBrains Junie, Devin (ex-Windsurf)
Set it up
This brain runs code automatically: 1 script. Review README.md before opening the folder in Claude Code. No step below turns a hook on for you.
README.md:- [Bun](https://bun.sh) v1.0+ — runtime for ClawMem. On Linux, install via `curl -fsSL https://bun.sh/install | bash` (not snap — snap Bun cannot read stdin, which breaks hooks).
- Not scanned by AutoGineerThe author’s MCP configuration (README.md) also starts it in a way AutoGineer does not offer. Read that file: AutoGineer did not scan that path; what it runs is listed under “This brain runs”. https://github.com/yoloshii/ClawMem/blob/284b55a3f23432d45412749c7717f32cfbdfbffa/README.md
- Not scanned by AutoGineerThe author’s README also describes installing it from a clone of its repository. Read its install section: AutoGineer did not scan that path. https://github.com/yoloshii/ClawMem/blob/284b55a3f23432d45412749c7717f32cfbdfbffa/README.md#install-from-source
- Not scanned by AutoGineerIts dependencies are installed by npm when you run it and were not scanned by AutoGineer (12 declared). https://www.npmjs.com/package/clawmem
First committed by yoloshii. Published in yoloshii/ClawMem. Licence: MIT License.
Usage
AutoGineer overview, from the brain’s own files
- Automatic context surfacing during agent prompts
- Persistent storage of session decisions and handoffs
- Targeted retrieval via MCP tools for cross-session questions
- Automated extraction of facts and causal links from transcripts
- “Why did we decide to use the current database schema?”
- The agent uses intent_search to traverse causal links and retrieve the specific decision record from past sessions.
- “What are the open questions from our last session?”
- The agent surfaces the session handoff and extracted open questions injected via the context-surfacing hook.
Documentation by yoloshii
ClawMem — On-device memory layer for Claude Code, OpenClaw, and Hermes agents
<p align="center"> <img src="docs/clawmem_hero.jpg" alt="ClawMem" width="100%"> </p>
On-device memory for Claude Code, OpenClaw, Hermes, and AI agents. Retrieval-augmented search, hooks, and an MCP server in a single local system. No API keys, no cloud dependencies.
ClawMem fuses recent research into a retrieval-augmented memory layer that agents actually use. The hybrid architecture combines QMD-derived multi-signal retrieval (BM25 + vector search + reciprocal rank fusion + query expansion + cross-encoder reranking), SAME-inspired composite scoring (recency decay, confidence, content-type half-lives, co-activation reinforcement), MAGMA-style intent classification with multi-graph traversal (semantic, temporal, and causal beam search), and A-MEM self-evolving memory notes that enrich documents with keywords, tags, and causal links between entries. Pattern extraction from Engram adds deduplication windows, frequency-based durability scoring, and temporal navigation.
Integrates via Claude Code hooks, an MCP server (works with any MCP-compatible client), a native OpenClaw plugin, or a Hermes Agent MemoryProvider plugin. All paths write to the same local SQLite vault. A decision captured during a Claude Code session shows up immediately when an OpenClaw or Hermes agent picks up the same project.
TypeScript on Bun. MIT License.
What It Does
ClawMem turns your markdown notes, project docs, and research dumps into persistent memory for AI coding agents. It automatically:
Surfaces relevant context on every prompt (context-surfacing hook)
Bootstraps sessions with your profile, latest handoff, recent decisions, and stale notes
Captures decisions, preferences, milestones, and problems from session transcripts using a local GGUF observer model
Imports conversation exports from Claude Code, ChatGPT, Claude.ai, Slack, and plain text via
clawmem mine, with optional post-import LLM fact extraction (--synthesize) that pulls structured decisions / preferences / milestones / problems and cross-fact links out of otherwise full-text conversation dumps (v0.7.2). Imports preserve authorship time (v0.27.0): ranking recency and temporal queries run on when content was actually written, so historical conversations mined today don't rank as fresh — with a metadata-only--backfill-dateslane for vaults mined earlierGenerates handoffs at session end so the next session can pick up where you left off
Learns what matters via a feedback loop that boosts referenced notes and decays unused ones
Guards against prompt injection in surfaced content
Classifies query intent (WHY / WHEN / ENTITY / WHAT) to weight search strategies
Traverses multi-graphs (semantic, temporal, causal) via adaptive beam search
Evolves memory metadata as new documents create or refine connections
Infers causal relationships between facts extracted from session observations
Detects contradictions between new and prior decisions, auto-decaying superseded ones — when a contradiction judge is configured (
CLAWMEM_JUDGE_*, v0.29.0; disabled otherwise) (with an additional merge-time contradiction gate in the consolidation worker that blocks cross-observation contradictions before they land, v0.7.1)Guards against cross-entity merges during consolidation — name-aware dual-threshold merge safety compares entity anchors before merging similar observations, preventing "Alice decided X" from merging into "Bob decided X" (v0.7.1)
Prevents context bleed in derived insights — the Phase 3 deductive synthesis pipeline validates every draft against an anti-contamination wrapper (deterministic entity contamination check + LLM validator + dedupe) before writing cross-session deductive observations (v0.7.1)
Frames surfaced facts as background knowledge —
context-surfacingwraps injected content in<instruction>+<facts>+<relationships>blocks, telling the model to treat facts as already-known and exposing memory-graph edges between surfaced docs directly in-prompt (v0.7.1)Injects knowledge-graph facts as structured triples — when the user's prompt mentions entities already known to the vault,
context-surfacingresolves them via a three-path prompt-only extractor (canonical IDs, proper nouns, lowercased n-grams), queries the SPO graph for current-state triples, and appends a<vault-facts>block of rawsubject predicate objectlines to<vault-context>— off forspeed, 200 tokens onbalanced, 250 ondeep, token-truncated at the triple boundary (v0.9.0)Session-scoped focus topic —
clawmem focus set "<topic>" --session-id <id>writes a per-session focus file used as a snippet-selection intent: surfaced docs prefer sentences matching the topic. Presentation only since v0.38.0 — a focus never changes expansion, reranking, scoring, ordering, or the surfaced set — session-isolated, fail-open, never writes to SQLite or lifecycle columns (v0.9.0, narrowed v0.38.0)Ranks hook injections on one channel-aware key — every retrieval leg (BM25, vector, file-aware, gated prior-turn, expansion variants) contributes a ranked lane; weighted reciprocal-rank fusion yields a single ordering key (current-support band, fused mass) that decides membership, final order, AND admission — no mixed raw-score sorts, no metadata reordering (v0.38.0)
Admits by relevance, abstains honestly — the context-surfacing hook admits candidates by a relative floor on the fused ordering key and abstains at query level when current-turn support is missing or keyword-degenerate, instead of surfacing weak lists (v0.38.0)
Guards the deep rerank lane — cross-encoder scores apply only under full candidate coverage AND a discriminating score set (degeneracy gate); the reranker joins the final order as a rank-fused lane, and remote rerank scores are cached only under an attested provider identity (
clawmem rerank-health) (v0.38.0)Keeps the hook's write path off your prompt latency — turn alignment is one early fail-closed row; recall attribution, token accounting, and vault mirrors are handed to a detached drainer process through an on-disk spool after the payload is emitted (v0.38.0)
Scores document quality using structure, keywords, and metadata richness signals
Boosts co-accessed documents — notes the agent verifiably referenced together in one turn (v0.41.0) get retrieval reinforcement on the composite MCP surfaces (removed from hook ordering in v0.38.0 — the injected order is the channel-aware fusion key alone)
Decomposes complex queries into typed retrieval clauses (BM25/vector/graph) for multi-topic questions
Cleans stale embeddings automatically before embed runs, removing orphans from deleted/changed documents
Transaction-safe indexing — crash mid-index leaves zero partial state (atomic commit with rollback)
Processes each turn once — the extraction and handoff hooks keep a cursor per transcript and the feedback hook decides each surfaced turn once (v0.41.0), so a turn is extracted, summarised and credited once instead of again at every response, and each session's documents are its own
Navigates temporal neighborhoods around any document via the
timelinetool — progressive disclosure from search to chronological context to full contentBoosts frequently-revised memories — documents with higher revision counts get a durability signal in composite scoring (capped at 10%)
Supports pin/snooze lifecycle for persistent boosts and temporary suppression
Manages document lifecycle — policy-driven archival sweeps with restore capability
Auto-routes queries via
memory_retrieve— classifies intent and dispatches to the optimal search backendSyncs project issues from Beads issue trackers into searchable memory
Runs a quiet-window heavy maintenance lane — a second consolidation worker, off by default behind
CLAWMEM_HEAVY_LANE=true, that runs on a longer interval only inside a configurable hour window. Gated bycontext_usagequery-rate so it never competes for CPU/GPU with interactive sessions, scoped exclusively via DB-backedworker_leases, stale-first by default with an optional surprisal selector, and journals every attempt inmaintenance_runsfor operator visibility (v0.8.0)
Runs fully local with no API keys and no cloud services. Integrates via Claude Code hooks and MCP tools, as a native OpenClaw plugin, or as a Hermes Agent MemoryProvider plugin. All modes share the same vault for cross-runtime memory. Works with any MCP-compatible client.
Full version history is in RELEASE_NOTES.md. Upgrade instructions for existing vaults are in docs/guides/upgrading.md.
Architecture
<p align="center"> <img src="docs/clawmem-architecture.png" alt="ClawMem Architecture" width="100%"> </p>
Install
Platform Support
| Platform | Status | Notes |
|---|---|---|
| Linux | Full support | Primary target. systemd services for watcher + embed timer. |
| macOS | Full support | Homebrew SQLite handled automatically. GPU via Metal (llama.cpp). |
| Windows (WSL2) | Full support | Recommended for Windows users. Install Bun + ClawMem inside WSL2. |
| Windows (native) | Not recommended | Bun and sqlite-vec work, but bin/clawmem wrapper is bash, hooks expect bash commands, and systemd services have no equivalent. Use WSL2 instead. |
Prerequisites
Required:
Bun v1.0+ — runtime for ClawMem. On Linux, install via
curl -fsSL https://bun.sh/install | bash(not snap — snap Bun cannot read stdin, which breaks hooks).SQLite with FTS5 — included with Bun. On macOS, install
brew install sqlitefor extension loading support (ClawMem detects and uses Homebrew SQLite automatically).
Optional (for better performance):
llama.cpp (
llama-server) — for dedicated GPU inference. Without it,node-llama-cppruns models in-process (auto-downloads on first use). GPU servers give better throughput and prevent silent CPU fallback.systemd (Linux) or launchd (macOS) — for persistent background services (watcher, embed timer, GPU servers). ClawMem ships systemd unit templates; macOS users can create equivalent launchd plists. See systemd services.
Optional integrations:
Claude Code — for hooks + MCP integration
OpenClaw — for native plugin integration
Hermes Agent — for
MemoryProviderplugin integrationbd CLI v0.58.0+ (verified through v1.1.0) — for Beads issue tracker sync (only if using Beads)
Install from npm (recommended)
npm install -g clawmemIf you use Bun as your package manager:
bun add -g clawmemInstall from source
git clone https://github.com/yoloshii/clawmem.git ~/clawmem
cd ~/clawmem && bun install
ln -sf ~/clawmem/bin/clawmem ~/.bun/bin/clawmemSetup roadmap
After installing, here's the full journey from zero to working memory:
| Step | What | How | Details |
|---|---|---|---|
| 1. Bootstrap | Create a vault, index your first collection, embed, install hooks and MCP | clawmem bootstrap ~/notes --name notes | One command does it all. Or run each step manually (see below). |
| 2. Choose models | Pick embedding + reranker models based on your hardware | 16GB+ VRAM → SOTA stack (zembed-1 + zerank-2). Less → QMD native combo. No GPU → cloud embedding or CPU fallback. | Inference services |
| 3. Download models | Get the model files for your chosen stack (GGUFs for all three services; for zerank-2, only a GGUF that carries its score head works) | wget from HuggingFace, let node-llama-cpp auto-download the QMD native models, or build the zerank-2 bf16 sidecar | Inference services |
| 4. Start services | Run GPU servers (if using dedicated GPU) and background services. Optionally enable the v0.8.2 background maintenance workers in the watcher unit so consolidation + deductive synthesis run automatically. | llama-server for each model. systemd units for watcher + embed timer. Drop-in for the watcher to enable workers + tune intervals + set the quiet window. | systemd services, background workers |
| 5. Decide what to index | Add collections for your projects, notes, research, and domain docs | clawmem collection add ~/project --name project | The more relevant markdown you index, the better retrieval works. See building a rich context field. |
| 6. Connect your agent | Hook into Claude Code, OpenClaw, Hermes, or any MCP client | clawmem setup hooks && clawmem setup mcp for Claude Code. clawmem setup openclaw for OpenClaw. Copy src/hermes/ to Hermes plugins for Hermes. | Integration |
| 7. Verify | Confirm everything is working | clawmem doctor (full health check) or clawmem status (quick index stats) | Verify Installation |
Fastest path: Step 1 alone gets you a working system with in-process CPU/GPU inference and default models — no manual model downloads or service configuration needed. Steps 2-4 are optional upgrades for better performance. Steps 5-6 are where you customize what gets indexed and how your agent connects.
Customize what gets indexed: Each collection has a pattern field in ~/.config/clawmem/config.yaml (default: **/*.md). Tailor it per collection — index project docs, research notes, decision records, Obsidian vaults, or anything else your agents should know about. The more relevant content in the vault, the better retrieval works. See the quickstart for config examples.
Quick start commands
# One command: init + index + embed + hooks + MCP
clawmem bootstrap ~/notes --name notes
# Or step by step:
clawmem init
clawmem collection add ~/notes --name notes
clawmem update --embed
clawmem setup hooks
clawmem setup mcp
# Add more collections (the more you index, the richer retrieval gets)
clawmem collection add ~/projects/myapp --name myapp
clawmem collection add ~/research --name research
clawmem update --embed
# Verify
clawmem doctorUpgrading
bun update -g clawmem # or: npm update -g clawmemDatabase schema migrates automatically on next startup (new tables and columns are added via CREATE IF NOT EXISTS / ALTER TABLE ADD COLUMN).
After major version updates (e.g. 0.1.x → 0.2.0) that add new enrichment pipelines, run a full enrichment pass to backfill existing documents:
clawmem reindex --enrich # Full enrichment: entity extraction (backfill) + links + evolution for all docs
clawmem embed # Re-embed if upgrading embedding models (not needed for most updates)--enrich runs the complete A-MEM pipeline (entity extraction, link generation, memory evolution) on all documents, not just new ones; entity extraction inside it skips a document unchanged since its last extraction (enrichment lifecycle). Without it, reindex only refreshes metadata for existing docs.
Routine patch updates (e.g. 0.2.0 → 0.2.1) do not require reindexing.
For version-specific upgrade notes (opt-in features, optional cleanup steps, verification commands), see docs/guides/upgrading.md.
Integration
Claude Code
ClawMem integrates via hooks (settings.json) and an MCP stdio server. Hooks handle 90% of retrieval automatically - the agent never needs to call tools for routine context.
clawmem setup hooks # Install lifecycle hooks (SessionStart, UserPromptSubmit, Stop, PreCompact)
clawmem setup mcp # Register MCP server in ~/.claude.json (33 tools)Automatic (90%): context-surfacing injects relevant memory on every prompt. postcompact-inject re-injects state after compaction. decision-extractor, handoff-generator, feedback-loop capture session state on stop.
Agent-initiated (10%): MCP tools (query, intent_search, find_causal_links, timeline, etc.) for targeted retrieval when hooks don't surface what's needed.
OpenClaw
ClawMem registers as a native OpenClaw memory plugin (kind: memory, v0.10.0+). Same 90/10 automatic retrieval, delivered through OpenClaw's plugin-hook bus instead of Claude Code hooks.
# v0.10.4+: profile-aware. Delegates to `openclaw plugins install --force` when the OpenClaw CLI
# is on PATH (auto-enables the plugin, honors OPENCLAW_STATE_DIR, OPENCLAW_CONFIG_PATH, --profile).
# Falls back to a recursive copy honoring OPENCLAW_STATE_DIR when the CLI is absent.
clawmem setup openclaw
# Custom profile (e.g. dev profile at ~/.openclaw-dev):
OPENCLAW_STATE_DIR=~/.openclaw-dev clawmem setup openclawWhat the plugin provides:
before_prompt_buildhook (load-bearing) - prompt-aware retrieval (context-surfacing + session-bootstrap) AND the pre-emptiveprecompact-extractrun when token usage approaches the compaction threshold. This is the authoritative path for precompact state capture because it runs synchronously before the LLM call that would trigger compaction, so it cannot race the compactor.agent_endhook - decision extraction, handoff generation, feedback loop (parallel, fire-and-forget at the OpenClaw call site). OpenClaw v2026.4.26+ also enforces a 30s default void-hook timeout onagent_end— slow handlers are logged but the underlying postrun work is not cancelled (fail-open).before_compactionhook (defense-in-depth fallback) - firesprecompact-extractagain for the rare case wherebefore_prompt_build's proximity heuristic missed a sudden token-count jump. Fire-and-forget at OpenClaw's call site, so it races the compactor and offers no correctness guarantee on its own — thebefore_prompt_buildpath is what actually holds the invariant.session_starthook - session registration + cached first-turn bootstrap context5 agent tools -
clawmem_search,clawmem_get,clawmem_session_log,clawmem_timeline,clawmem_similar
Disable OpenClaw's native memory search to avoid duplicate injection:
openclaw config set agents.defaults.memorySearch.extraPaths "[]"ClawMem coexists cleanly with OpenClaw's Active Memory plugin (v2026.4.10+) and, on OpenClaw v2026.4.18+ (#65411), with the memory-core dreaming sidecar — both run alongside ClawMem instead of being mutually exclusive. They search different backends and inject into different prompt regions, so they do not conflict. See the OpenClaw plugin guide — Active Memory coexistence and the memory-core dreaming sidecar section for the two patterns.
Pair ClawMem (memory) with a context-engine plugin (v0.10.0+). OpenClaw and Hermes maintainers have converged on a two-surface plugin model: one slot for memory plugins (cross-session, retrieval-first) and a separate slot for context-engine plugins (in-session, compression/compaction-first). Under that model ClawMem is a memory layer — it has always been one in Hermes via the MemoryProvider ABC, and v0.10.0 moves the OpenClaw integration to the same semantic slot. You can now run ClawMem in the memory slot alongside an LCM-style compression plugin (for example, lossless-claw) in the context-engine slot. The two plugins do not overlap: one persists across sessions, the other reshapes the live window. See the OpenClaw plugin guide — memory vs context engine for the full rationale.
OpenClaw v2026.4.11+ recommended (required for ClawMem v0.10.0+). v2026.4.11 introduced a new plugin discovery contract that requires each plugin directory to ship a
package.jsonwithopenclaw.extensionsdeclared, and that rejects symlinked plugin directories. ClawMem v0.10.0 includes both fixes. Older ClawMem versions (< v0.10.0) on OpenClaw v2026.4.11+ will fail to discover silently — upgrade ClawMem, then re-runclawmem setup openclaw. See docs/guides/upgrading.md.
Alternative: OpenClaw agents can also use ClawMem's MCP server directly (clawmem setup mcp), with or without hooks. This gives full access to all 33 MCP tools but bypasses OpenClaw's plugin lifecycle, so you lose token budget awareness, native compaction orchestration, and the agent_end message pipeline. The native OpenClaw plugin is recommended for new setups; MCP is available as an additional or standalone integration.
Hermes Agent
ClawMem integrates as a native MemoryProvider plugin — Hermes's pluggable interface for agent memory. Same automatic retrieval and extraction, delivered through Hermes's memory lifecycle instead of Claude Code hooks.
Install:
# Preferred — user-plugin path (Hermes #10529, v2026.4.13+).
# Survives `git pull` of hermes-agent and avoids dual-registration with bundled providers.
# Copies the directory's CONTENTS, so the same command installs and upgrades.
mkdir -p "${HERMES_HOME:-$HOME/.hermes}/plugins/clawmem"
cp -r /path/to/ClawMem/src/hermes/. "${HERMES_HOME:-$HOME/.hermes}/plugins/clawmem/"
# Or, the bundled-style path (always supported, takes precedence on name collisions).
# Recommended only when you actively work in the hermes-agent source tree.
mkdir -p /path/to/hermes-agent/plugins/memory/clawmem
cp -r /path/to/ClawMem/src/hermes/. /path/to/hermes-agent/plugins/memory/clawmem/
# Symlink alternative for in-place development (either path).
ln -s /path/to/ClawMem/src/hermes ${HERMES_HOME:-~/.hermes}/plugins/clawmemConfigure in your Hermes profile's .env or environment:
CLAWMEM_BIN=/path/to/clawmem # Path to clawmem binary (or ensure it's on PATH)
CLAWMEM_SERVE_PORT=7438 # REST API port (default: 7438)
CLAWMEM_SERVE_MODE=external # "external" (you run clawmem serve) or "managed" (plugin manages it)
CLAWMEM_PROFILE=balanced # speed | balanced | deepThen set memory.provider: clawmem in your Hermes config.yaml, or run hermes memory setup to configure interactively.
What the plugin provides:
prefetch()— prompt-aware retrieval viacontext-surfacinghook (automatic every turn)sync_turn()— appends the turn to the plugin's transcript, then runs decision extraction, handoff generation and the feedback loop over it in the background (v0.41.0: after every turn; each turn is processed once)on_session_end()— a last pass, then the SessionEnd handoff flushon_pre_compress()— pre-compaction state preservationsession-bootstrap— session registration + first-turn context injection5 agent tools —
clawmem_retrieve,clawmem_get,clawmem_session_log,clawmem_timeline,clawmem_similarPlugin-managed transcript — maintains its own JSONL transcript for ClawMem hooks
Requirements: clawmem binary on PATH and clawmem serve running (external mode) or the plugin starts it automatically (managed mode). Python 3.10+. No pip dependencies beyond Hermes itself (uses urllib for REST calls, httpx optional for better performance).
Alternative: Hermes also has built-in MCP client support. You can add ClawMem as an MCP server in Hermes's config.yaml under mcp_servers for tool-only access. But this misses the lifecycle hooks (prefetch, session_end, pre_compress), so the native plugin is recommended.
See Hermes plugin guide for architecture details, lifecycle mapping, and troubleshooting.
Multi-Framework Operation
All three integrations share the same SQLite vault by default. Claude Code, OpenClaw, and Hermes can run simultaneously — decisions captured in one runtime are immediately available in the others, giving agents persistent shared memory across sessions and platforms. WAL mode + busy_timeout handles concurrent access.
Multi-Vault (Optional)
By default, ClawMem uses a single vault at ~/.cache/clawmem/index.sqlite. For users who want separate memory domains (e.g., work vs personal, or isolated vaults per project), ClawMem supports named vaults.
Configure in ~/.config/clawmem/config.yaml:
vaults:
work: ~/.cache/clawmem/work.sqlite
personal: ~/.cache/clawmem/personal.sqliteOr via environment variable:
export CLAWMEM_VAULTS='{"work":"~/.cache/clawmem/work.sqlite","personal":"~/.cache/clawmem/personal.sqlite"}'Using vaults with MCP tools:
All retrieval tools (memory_retrieve, query, search, vsearch, intent_search) accept an optional vault parameter. Omit it to use the default vault.
# Search the default vault (no vault param needed)
query("authentication flow")
# Search a named vault
query("project timeline", vault="work")
# List configured vaults
list_vaults()
# Sync content into a vault
vault_sync(vault="work", content_root="~/work/docs")Single-vault users: No action needed. Everything works without configuration. The vault parameter is always optional and ignored when no vaults are configured.
Inference services (GPU, in-process, or cloud)
ClawMem uses three inference services — embedding, LLM (query expansion / intent / A-MEM / the Stop hooks' observer), and reranker. In the default stack all three run as llama-server instances, each with an in-process node-llama-cpp fallback that auto-downloads on first use, so ClawMem works without a dedicated GPU (Metal on Apple Silicon, Vulkan where available, CPU as last resort). The bin/clawmem wrapper points at localhost:8088/8089/8090. Always run via bin/clawmem — it sets the endpoints.
Choose a stack:
| Stack | Models | VRAM | License | When |
|---|---|---|---|---|
| QMD native (default) | EmbeddingGemma-300M + qmd-query-expansion-1.7B + qwen3-reranker-0.6B | ~4 GB, or in-process | Permissive — commercial OK | Any GPU or none; zero-config start |
| z / SOTA | zembed-1 + qmd-query-expansion-1.7B + zerank-2 (Q8_0 GGUF that carries its score head) | ~13 GB | Apache-2.0 — commercial OK | 16 GB+ GPU; best recall |
| Cloud embedding | Jina / OpenAI / Voyage / Cohere (embedding only) | none | provider ToS | No local GPU for embedding; LLM + reranker stay local |
Heads-up before you serve: most zerank-2 GGUFs are inert — llama.cpp's standard converter drops the score head. Serve the Q8_0 GGUF that carries it (launch line) or the bf16 seq-cls sidecar, and verify with clawmem rerank-health. zembed-1 / reranking need -ub = -b (non-causal attention). Changing embedding dimensions requires clawmem embed --force. Set CLAWMEM_NO_LOCAL_MODELS=true to fail fast instead of silent CPU fallback.
→ Full stack decision matrix, model/VRAM tables, and server-setup commands: docs/guides/inference-services.md. Cloud providers, batch + TPM behavior: docs/guides/cloud-embedding.md. All environment variables: docs/reference/configuration.md. Keeping servers running: docs/guides/systemd-services.md.
MCP Server
ClawMem exposes 33 MCP tools via the Model Context Protocol and an optional HTTP REST API. Any MCP-compatible client or HTTP client can use it.
Claude Code (automatic):
./bin/clawmem setup mcp # Registers in ~/.claude.jsonManual (any MCP client):
Add to your MCP config (e.g. ~/.claude.json, claude_desktop_config.json, or your client's equivalent):
{
"mcpServers": {
"clawmem": {
"command": "/absolute/path/to/clawmem/bin/clawmem",
"args": ["mcp"]
}
}
}The server runs via stdio — no network port needed. The bin/clawmem wrapper sets the GPU endpoint env vars automatically.
Verify: After registering, your client should see tools including memory_retrieve, search, vsearch, query, query_plan, intent_search, timeline, etc.
HTTP REST API (optional)
For web dashboards, non-MCP agents, cross-machine access, or programmatic use:
./bin/clawmem serve # localhost:7438; every request needs the token (below)
./bin/clawmem serve --port 8080 # custom port
./bin/clawmem serve-token # print the token serve usesEndpoints:
| Method | Path | Description |
|---|---|---|
| GET | /health | Liveness probe + version + doc count |
| GET | /stats | Full index statistics |
| POST | /search | Unified search (mode: auto/keyword/semantic/hybrid) |
| POST | /retrieve | Smart retrieve with auto-routing (mode: auto/keyword/semantic/causal/timeline/hybrid) |
| GET | /documents/:docid | Single document by 6-char hash prefix |
| GET | /documents?pattern=... | Multi-get by glob pattern |
| GET | /timeline/:docid | Temporal neighborhood (before/after) |
| GET | /sessions | Recent session history |
| GET | /collections | List all collections |
| GET | /lifecycle/status | Active/archived/forgotten/pinned/snoozed counts + deactivation_reasons (absent/forget/archive/unknown_legacy) |
| POST | /documents/:docid/pin | Pin/unpin |
| POST | /documents/:docid/snooze | Snooze until date |
| POST | /documents/:docid/forget | Deactivate |
| POST | /lifecycle/sweep | Archive stale docs (dry_run default) |
| GET | /graph/causal/:docid | Causal chain traversal |
| GET | /graph/similar/:docid | k-NN neighbors |
| GET | /export | Active-document export as JSON (?full=true includes legacy precompact-state.md snapshot copies) |
| POST | /reindex | Trigger re-scan |
| POST | /graphs/build | Rebuild temporal + semantic graphs |
Auth (v0.42.0): every request needs Authorization: Bearer <token>: CLAWMEM_API_TOKEN when set (32+ characters), otherwise the token file serve generates at ~/.config/clawmem/serve-token; clawmem serve-token prints it. Every POST needs Content-Type: application/json, and requests from web pages (a foreign Origin or Host) are refused. See the REST API reference.
Search example:
curl -X POST http://localhost:7438/search \
-H "Authorization: Bearer $(clawmem serve-token)" \
-H 'Content-Type: application/json' \
-d '{"query": "authentication decisions", "mode": "hybrid", "compact": true}'Verify Installation
./bin/clawmem doctor # Full health check
./bin/clawmem status # Quick index status
bun test # Run test suite (from the repository root)Agent Instructions
ClawMem ships agent instruction files and an optional maintenance agent:
| File | Loaded | Purpose |
|---|---|---|
AGENTS.md | Automatically (most agents, when working in this repo) | Lean root SSOT — agent-facing quick reference (inference at a glance, install, retrieval routing, MCP tools, indexing rules, integrations) + an index into docs/ |
CLAUDE.md | Automatically (Claude Code) | @AGENTS.md import — Claude Code reads CLAUDE.md natively, so it just pulls in the SSOT |
SKILL.md | On-demand (agent reads when needed) | Portable operating reference — escalation gate, tool routing, the 4 query-optimization levers, pipeline behavior, composite scoring, lifecycle. Shipped with the package for cross-project use |
docs/ | On-demand (linked throughout AGENTS.md) | Deep reference — inference setup, configuration, concepts, internals, guides, troubleshooting |
agents/clawmem-curator.md | On-demand via clawmem setup curator | Maintenance agent — lifecycle triage, retrieval health checks, dedup sweeps, graph rebuilds |
Working in the ClawMem repo: No action needed — CLAUDE.md (which imports AGENTS.md) loads automatically.
Using ClawMem from other projects: Your agent needs instructions on how to use ClawMem's hooks and MCP tools. Two options:
Option A: Copy instructions into your project
Copy the contents of AGENTS.md (or the relevant sections) into your project's own AGENTS.md or CLAUDE.md. Simple but requires manual updates when ClawMem changes.
Option B: Add a trigger block (recommended)
Add this minimal trigger block to your global ~/.claude/CLAUDE.md. It gives the agent routing rules always loaded, and tells it how to find the full reference (SKILL.md) shipped with your installation when deeper guidance is needed:
## ClawMem
Architecture: hooks (automatic, ~90%) + MCP tools (explicit, ~10%).
Vault: `~/.cache/clawmem/index.sqlite` | Config: `~/.config/clawmem/config.yaml`
### Escalation Gate (3 rules — ONLY escalate to MCP tools when one fires)
1. **Low-specificity injection** — `<vault-context>` is empty or lacks the specific fact needed
2. **Cross-session question** — "why did we decide X", "what changed since last time"
3. **Pre-irreversible check** — before destructive or hard-to-reverse changes
### Tool Routing (once escalated)
**Preferred:** `memory_retrieve(query)` — auto-classifies and routes to the optimal backend.
**Direct routing** (when calling specific tools):
"why did we decide X" → intent_search(query) NOT query()
"what happened last session" → session_log() NOT query()
"what else relates to X" → find_similar(file) NOT query()
Complex multi-topic → query_plan(query) NOT query()
General recall → query(query, compact=true)
Keyword spot check → search(query, compact=true)
Conceptual/fuzzy → vsearch(query, compact=true)
Full content → multi_get("path1,path2")
Lifecycle health → lifecycle_status()
Stale sweep → lifecycle_sweep(dry_run=true)
Restore archived → lifecycle_restore(query)
ALWAYS `compact=true` first → review → `multi_get` for full content.
### Proactive Use (no escalation gate needed)
- User says "remember this" / critical decision made → `memory_pin(query)` immediately
- User corrects a misconception → `memory_pin(query)` the correction
- `<vault-context>` surfaces irrelevant/noisy content → `memory_snooze(query, until)` for 30 days
- Need to correct a memory → `memory_forget(query)`
- After bulk ingestion → `build_graphs`
### Anti-Patterns
- Do NOT use `query()` for everything — match query type to tool, or use `memory_retrieve`
- Do NOT call query/intent_search every turn — 3 rules above are the only gates
- Do NOT re-search what's already in `<vault-context>`
- Do NOT pin everything — pin is for persistent high-priority items, not routine decisions
- Do NOT forget memories to "clean up" — let confidence decay handle it
- Do NOT wait for curator to pin decisions — pin immediately when critical
For detailed operating guidance (escalation gate, tool routing, query-optimization levers, pipeline behavior, composite scoring, lifecycle), find and read the shipped SKILL.md (setup, inference, and internals live in AGENTS.md + docs/):
Bash: CLAWMEM_ROOT=$(cd "$(dirname "$(which clawmem)")/.." && pwd) && echo "$CLAWMEM_ROOT/SKILL.md"
Then: Read the file at that path.This gives your agent the 3-rule gate, tool routing, and proactive behaviors always loaded. When it needs deeper guidance, it locates and reads the full SKILL.md reference shipped with your installation — no symlinks or skill registration required.
CLI Reference
clawmem init Create DB + config
clawmem bootstrap <vault> [--name N] [--skip-embed] One-command setup
clawmem collection add <path> --name <name> Add a collection
clawmem collection list List collections
clawmem collection remove <name> Remove a collection
clawmem update [--pull] [--embed] Incremental re-scan
clawmem mine <dir> [-c name] [--embed] [--synthesize] Import conversation exports (--synthesize runs post-import LLM fact extraction, v0.7.2; preserves per-exchange authored_at, v0.27.0)
clawmem mine <dir> -c name --backfill-dates [--apply] Derive authored_at for already-mined docs (metadata-only; dry-run without --apply)
clawmem embed [-f] Generate fragment embeddings
clawmem reindex [--force] Full re-index
clawmem watch File watcher daemon
clawmem search <query> [-n N] [--json] BM25 keyword search
clawmem vsearch <query> [-n N] [--json] Vector semantic search
clawmem query <query> [-n N] [--json] Full hybrid pipeline
clawmem profile Show user profile
clawmem profile rebuild Force profile rebuild
clawmem update-context Regenerate per-folder CLAUDE.md
clawmem list [-n/--limit N] [-c col] [--json] Browse recent documents
clawmem budget [--session ID] Token utilization
clawmem log [--last N] Session history
clawmem hook <name> Manual hook trigger
clawmem surface --context --stdin IO6: pre-prompt context injection
clawmem surface --bootstrap --stdin IO6: per-session bootstrap injection
clawmem reflect [N] Cross-session reflection (last N days, default 14)
clawmem consolidate [--dry-run] [N] Find and archive duplicate low-confidence docs
clawmem eval run --gold <file.jsonl> Offline eval: replay gold-labeled queries, score J_doc/recall/MRR
clawmem install-service [--enable] [--remove] Systemd watcher service
clawmem setup hooks [--remove] Install/remove Claude Code hooks
clawmem setup mcp [--remove] Register/remove MCP server
clawmem setup curator [--remove] Install/remove curator maintenance agent
clawmem mcp Start stdio MCP server
clawmem serve [--port 7438] [--host 127.0.0.1] [--no-token] Start HTTP REST API server (token required)
clawmem serve-token Print the token serve uses
clawmem path Print database path
clawmem doctor Full health check
clawmem status Quick index statusMCP Tools (31)
Registered by clawmem setup mcp. Available to any MCP-compatible client.
Internal-collection visibility (v0.21.0): the retrieval tools search, vsearch, query, query_plan, memory_retrieve, and find_similar (plus memory_rank since v0.36.0) exclude the system-internal _clawmem collection by default — pass includeInternal: true (or name _clawmem in an explicit collection filter where the tool has one) to include it. find_similar auto-includes internal neighbors when the reference document is itself internal; intent_search, find_causal_links, kg_query, session_log, and timeline are unfiltered by design. Full contract (including the degraded under-fill markers): docs/reference/mcp-tools.md.
| Tool | Description |
|---|---|
__IMPORTANT | Workflow guide: prefer memory_retrieve → match query type to tool → multi_get for full content |
Core Search & Retrieval
| Tool | Description |
|---|---|
memory_retrieve | Preferred entry point. Auto-classifies query and routes to optimal backend (query, intent_search, session_log, find_similar, or query_plan). Use instead of manually choosing a search tool. |
search | BM25 keyword search — for exact terms, config names, error codes, filenames. v0.24.0: non-recency queries rank by the RAW BM25 transform (scoreBasis: "fts-bm25"; judged keyword eval MRR 0.848 vs composite 0.415; metadata breaks exact ties only; minScore filters raw, no default); recency-intent queries keep composite. Collection filter supports comma-separated values. Prefer memory_retrieve for auto-routing. |
vsearch | Vector semantic search — for conceptual/fuzzy matching when exact keywords are unknown. v0.22.0: non-recency queries rank by RAW cosine (scoreBasis: "vector-cosine"; metadata breaks exact ties only; minScore filters raw with no default); recency-intent queries keep composite. Collection filter supports comma-separated values. Prefer memory_retrieve for auto-routing. |
query | Full hybrid pipeline (BM25 + vector + rerank) — general-purpose when query type is unclear. WRONG for "why" questions (use intent_search) or cross-session queries (use session_log). Prefer memory_retrieve for auto-routing. Intent hint, strong-signal bypass, chunk dedup, candidateLimit, MMR diversity, compact mode. |
get | Retrieve single document by path or docid |
multi_get | Retrieve multiple docs by glob or comma-separated list |
find_similar | USE THIS for "what else relates to X", "show me similar docs". Finds k-NN vector neighbors — discovers connections beyond keyword overlap that search/query cannot find. |
Intent-Aware Search
| Tool | Description |
|---|---|
intent_search | USE THIS for "why did we decide X", "what caused Y", "who worked on Z". Classifies intent (WHY/WHEN/ENTITY/WHAT), traverses causal + semantic graph edges. Returns decision chains that query() cannot find. |
query_plan | USE THIS for complex multi-topic queries ("tell me about X and also Y", "compare A with B"). Decomposes into parallel typed clauses (bm25/vector/graph), executes each, merges via RRF. query() searches as one blob — this tool splits topics and routes each optimally. |
intent_search pipeline: Query → Intent Classification → BM25 + Vector → Intent-Weighted RRF → Graph Expansion (WHY/ENTITY intents) → Cross-Encoder Reranking → Composite Scoring
query_plan pipeline: Query → LLM decomposition into 2-4 typed clauses → Parallel execution (BM25/vector/graph per clause) → RRF merge across clauses → Composite scoring. Falls back to single-query for simple inputs.
Multi-Graph & Causal
| Tool | Description |
|---|---|
build_graphs | Build temporal and/or semantic graphs from document corpus |
find_causal_links | Trace decision chains: "what led to X", "how we got from A to B". Follow up intent_search with this tool on a top result to walk the full causal chain. Traverses causes / caused_by / both up to N hops with depth-annotated reasoning. |
kg_query | Query the SPO knowledge graph: "what does X relate to?", "what was true about X when?". Returns temporal entity-relationship triples with validity windows. Accepts an entity name or a canonical ID in vault:type:slug form; an exact name wins over a partial match, and entities that share the name are listed separately (resolution). Triples are populated by the decision-extractor hook from observer-emitted <triples> blocks. |
memory_evolution_status | Show how a document's A-MEM metadata evolved over time |
timeline | Show the temporal neighborhood around a document — what was created/modified before and after it. Progressive disclosure: search → timeline (context) → get (full content). Supports same-collection scoping and session correlation. |
Beads Integration
| Tool | Description |
|---|---|
beads_sync | Sync Beads issues from Dolt backend (bd CLI) into memory: syncs the full backlog (no 50-issue cap), creates docs, bridges all dep types to memory_relations, surfaces bd ≥1.1.0 claim leases, runs A-MEM enrichment |
Vault Management
| Tool | Description |
|---|---|
list_vaults | Show configured vault names and paths. Empty in single-vault mode. |
vault_sync | Index markdown from a directory into a named vault. Restricted-path validation rejects sensitive directories. |
Agent Diary
| Tool | Description |
|---|---|
diary_write | Write a diary entry. Use for recording important events, decisions, or observations in environments without hook support. Stored as searchable memories. |
diary_read | Read recent diary entries. Filter by agent name. |
Memory Management & Lifecycle
| Tool | Description |
|---|---|
memory_forget | Search → deactivate closest match (with audit trail). Weak matches return a disambiguation list instead of acting (v0.23.0) |
memory_pin | Pin a memory: lifecycle retention + priority among relevance-equivalent results (+0.3 composite boost on hook/query surfaces; exact-tie precedence on the raw routes — vector + search non-recency). USE PROACTIVELY when: user states a persistent constraint, makes an architecture decision, or corrects a misconception. Don't wait for curator — pin critical decisions immediately. |
memory_snooze | Temporarily hide a memory from context surfacing until a date. USE PROACTIVELY when <vault-context> repeatedly surfaces irrelevant content — snooze for 30 days instead of ignoring it. |
status | Index health with content type distribution |
reindex | Trigger vault re-scan |
index_stats | Detailed stats: types, staleness, access counts, sessions |
memory_stats | Lifecycle + ranking-metadata aggregates per collection (v0.36.0): origin×active cross-tabs, pinned, accrual, deactivation reasons, and access/confidence/quality/effective-age distributions (mean/median/min/max over active rows). Deeper than index_stats; includes system collections. |
memory_rank | Ranking diagnostic (v0.36.0): real-pipeline composite breakdown per result — weights, recency, confidence blend, quality/length/frequency/canonical multipliers, signed pinΔ, co-activation — plus raw-vs-composite rank shifts; demoted raw winners stay visible. FTS-only candidates; read-only. |
session_log | USE THIS for "last time", "yesterday", "what happened", "what did we do". Returns session history with handoffs and file changes. DO NOT use query() for cross-session questions — this tool has session-specific data that search cannot find. |
profile | Current static + dynamic user profile |
lifecycle_status | Document lifecycle statistics: active, archived, forgotten, pinned, snoozed counts, the deactivation-reason breakdown, and policy summary |
lifecycle_sweep | Run lifecycle policies: archive stale docs past retention threshold. Archives only — never deletes. Defaults to dry_run (preview only) |
lifecycle_restore | Restore documents that were auto-archived by lifecycle policies. Filter by query, collection, or restore all |
ClawMem never physically deletes a document row (v0.30.0). Forget and archive deactivate; lifecycle_restore reverses archival; purge_after_days is inert. Through v0.29.0 a configured retention window permanently deleted archived rows from a non-dry-run sweep and from the SessionStart hook, without reporting it — see upgrading.
Compact Mode
search, vsearch, and query accept compact: true to return { id, path, title, score, snippet, content_type, fragment } instead of full content. Saves ~5x tokens for initial filtering.
Hooks (Claude Code Integration)
Hooks installed by clawmem setup hooks:
| Hook | Event | What It Does |
|---|---|---|
context-surfacing | UserPromptSubmit | Retrieval gate → lane-based hybrid search (BM25 + vector + file-aware (E13) + gated prior-turn + deep expansion variants) → weighted RRF fusion onto one channel-aware ordering key → snooze/noise filters → relevance admission with query-level abstention (v0.38.0) → tiered injection (HOT/WARM/COLD) → <vault-context> + <vault-routing> hint. Profile-driven budget/results/timeout; CLAWMEM_HOOK_BUDGET_MS is the authoritative internal deadline. |
postcompact-inject | SessionStart (compact) | Re-injects context after compaction, framed as reference data: this session's pre-compaction state + recent decisions + antipatterns + vault context (1200 token budget) |
curator-nudge | SessionStart | Surfaces curator report actions, nudges when report is stale (>7 days) |
precompact-extract | PreCompact | Extracts the last typed request, decisions, file paths and open questions before compaction → the vault's session-keyed compaction_state row, taken once by the same session's postcompact-inject |
decision-extractor | Stop | GGUF observer extracts structured observations (decisions, preferences, milestones, problems, bugfixes, features, refactors, discoveries) from the turns after its per-transcript cursor, each turn once (v0.41.0), into the session's own decision and antipattern docs; detects contradictions with prior decisions (judge-gated — requires CLAWMEM_JUDGE_*, v0.29.0) |
handoff-generator | Stop | Digests each new turn without a model, then folds the digests into an incremental, throttled observer summary → the session's handoff |
handoff-generator | SessionEnd | Renders the handoff's latest turns at session end (no transcript read, no model; v0.41.0) |
feedback-loop | Stop | Silently credits each surfaced note once per turn when that turn verifiably names it (path, file name or rendered title): access count, co-activation + usage relations among the notes that turn referenced, utility signals (surfaced vs referenced ratio for lifecycle automation) |
Additional hooks available but not installed by default:
| Hook | Event | Why Not Default |
|---|---|---|
session-bootstrap | SessionStart | Injects ~2000 tokens before user types anything. context-surfacing on first prompt is more precise. |
staleness-check | SessionStart | Redundant without session-bootstrap (stale notes are part of its output). |
pretool-inject | PreToolUse | Disabled in HOOK_EVENT_MAP (cannot inject additionalContext via PreToolUse). |
Hooks handle ~90% of retrieval automatically. For agent escalation logic (when to use MCP tools vs rely on hooks), see CLAUDE.md.
Search Pipeline
User Query + optional intent hint
→ BM25 Probe → Strong Signal Check (skip expansion if top hit ≥ 0.85 with gap ≥ 0.15; disabled when intent provided)
→ Query Expansion (intent steers LLM prompt when provided)
→ BM25 + Vector Search (parallel, original query 2× weight)
→ Reciprocal Rank Fusion → slice to candidateLimit (default 30)
→ Intent-Aware Chunk Selection (intent terms at 0.5× weight alongside query terms at 1.0×)
→ Cross-Encoder Reranking (4000 char context; intent prepended; chunk dedup; batch cap=4)
→ Rerank/RRF Blend (blendRerank: 0.9·reranker + 0.1·normalized-RRF tiebreaker; reranker can promote over RRF #1; falls back to RRF order if reranker unavailable)
→ Composite Scoring (query-tuned: (search × 0.70 + recency × 0.15 + confidence × 0.15) × qualityMultiplier × lengthNorm × coActivationBoost + pinBoost; recency-intent queries → 0.10/0.70/0.20; other tools keep the 0.50/0.25/0.25 default)
→ MMR Diversity Filter (Jaccard bigram similarity > 0.6 → demoted)
→ Ranked ResultsFor agent-facing query optimization (tool selection, query string quality, intent parameter, candidateLimit), see CLAUDE.md.
Multi-Graph Traversal
For WHY and ENTITY queries, the search pipeline expands results through the memory graph:
Start from top-10 baseline results as anchor nodes
For each frontier node: get neighbors via any relation type
Score transitions:
λ1·structure + λ2·semantic_affinityApply decay:
new_score = parent_score * γ + transition_scoreKeep top-k (beam search), repeat until max depth or budget
Graph types:
Semantic — vector similarity edges (threshold > 0.7)
Temporal — chronological document ordering
Causal — LLM-inferred cause→effect from Observer facts + Beads
blocks/waits-fordepsSupporting — LLM-analyzed document relationships + Beads
discovered-fromdepsContradicts — LLM-analyzed document relationships. Additional
contradictsedges are inserted by Phase 3 deductive synthesis when a new draft contradicts a prior deductive observation. (Phase 2's merge-time gate does not insert edges — a blocked merge underlinksets the old consolidated row'sinvalidated_bybacklink instead.)
Content Type Scoring
| Type | Half-life | Baseline | Notes |
|---|---|---|---|
decision | 180d | 0.85 | Very slow ranking decay (§36.11) — silently-abandoned decisions stop winning ranking; attention-decay-exempt; lifecycle policy separate |
deductive | ∞ | 0.85 | Never decays — cross-session derived insights with source provenance |
preference | ∞ | 0.80 | Never decays — user preferences are durable facts |
hub | ∞ | 0.80 | Never decays |
antipattern | ∞ | 0.75 | Never decays — accumulated negative patterns persist |
problem | 60 days | 0.75 | High priority but resolves over time |
research | 90 days | 0.70 | |
milestone | 60 days | 0.70 | Important at the time, fades as project moves forward |
project | 120 days | 0.65 | |
handoff | 30 days | 0.60 | Fast decay — most recent matters |
conversation | 45 days | 0.55 | Imported chat exchanges |
progress | 45 days | 0.50 | |
note | 60 days | 0.50 | Default |
Content types are inferred from frontmatter or file path patterns. Half-lives extend up to 3× for frequently-accessed memories (access reinforcement, decays over 90 days). Non-durable types (handoff, progress, conversation, note, project) lose 5% confidence per week without access (attention decay). Decision/deductive/preference/hub/research/antipattern are exempt.
Quality scoring: Each document gets a quality_score (0.0–1.0) computed during indexing based on length, structure (headings, lists), decision/correction keywords, and frontmatter richness. Applied as qualityMultiplier = 0.7 + 0.6 × qualityScore (range: 0.7× penalty to 1.3× boost).
Length normalization: 1/(1 + 0.5 × log2(max(bodyLength/500, 1))) — penalizes verbose entries that dominate via keyword density. Floor at 30% of original score.
Frequency boost: Documents with higher revision counts or duplicate counts get a durability signal: freqSignal = (revisions - 1) × 2 + (duplicates - 1), freqBoost = min(0.10, log1p(freqSignal) × 0.03). Revision count (content evolution) is weighted 2× vs duplicate count (ingest repetition). Capped at 10%.
Pin boost: Pin = lifecycle retention + priority among relevance-equivalent results. On composite surfaces pinned documents get a +0.3 additive boost (capped at 1.0); on the raw routes — the vector tools (v0.22.0) and non-recency search (v0.24.0) — pin breaks exact raw-score ties only. Use memory_pin to pin critical memories.
Snooze: Snoozed documents are filtered out of context surfacing until their snooze date. Use memory_snooze for temporary suppression.
Contradiction detection (judge-gated, v0.29.0): Contradiction analysis runs only when a judge is configured via CLAWMEM_JUDGE_* — without one it is disabled as an audited no-op, because the stock expansion model cannot meet the judge contract (details). With a judge: when decision-extractor identifies a new decision that contradicts a prior one, the old decision's confidence is automatically lowered (−0.25 for contradictions, −0.15 for updates). Superseded decisions naturally fade from context surfacing without manual intervention. When erosion reaches the 0.2 floor the hook can additionally set invalidated_at, which removes the document from FTS and vector retrieval entirely. That step applies only to content_type='observation' rows — a superseded decision is eroded but never retired — and is off by default behind CLAWMEM_CONTRADICTION_INVALIDATE and logs WOULD invalidate instead of writing, because its exposure depends on the vault's confidence distribution and content-type mix (calibration guide). Two contract defects fixed in v0.28.0 had previously prevented this path from applying any classification at all — earlier versions classified but never mutated a document. v0.7.1 adds a second layer at the consolidation boundary: before the background worker merges a new pattern into an existing consolidated observation, a deterministic heuristic (negation asymmetry, number/date mismatch) plus the configured judge detect contradictions (heuristic-only, and constrained to link, when no judge is configured). Blocked merges route to either link policy (both rows remain active; the old row gains an invalidated_by backlink, default) or supersede policy (old row marked status='inactive' — requires a configured judge, otherwise loudly constrained to link). Configurable via CLAWMEM_CONTRADICTION_POLICY and CLAWMEM_CONTRADICTION_MIN_CONFIDENCE. Mutation-authorizing evaluations commit their durable judge_runs/judge_events audit rows in the same transaction as the mutation (Phase-3 deductive checks commit a precondition audit first; non-mutating outcomes write standalone rows) — that audit is what calibration reads. Data-egress note: a cloud or subscription judge receives the new decisions and the retrieved candidate snippets.
Features
A-MEM (Adaptive Memory Evolution)
Documents are automatically enriched with structured metadata when indexed:
Keywords (3-7 specific terms)
Tags (3-5 broad categories)
Context (1-2 sentence description)
When new documents create links, neighboring documents' metadata evolves — keywords merge, context updates, and the evolution history is tracked with version numbers and reasoning.
Causal Inference
The decision-extractor hook analyzes Observer facts for causal relationships. When multiple facts exist in an observation, an LLM identifies cause→effect pairs (confidence ≥ 0.6). Causal chains can be queried via find_causal_links with multi-hop traversal using recursive CTEs.
Beads Integration
Projects using Beads (v0.58.0+, Dolt backend) issue tracking are fully integrated into the MAGMA memory graph:
Auto-sync: Watcher detects
.beads/directory changes →syncBeadsIssues()queriesbdCLI for live Dolt data → creates markdown docs inbeadscollectionDependency bridging: All Beads dependency types map to
memory_relationsedges —blocks/conditional-blocks/waits-for/caused-by→causal,discovered-from/supersedes/duplicates→supporting,relates-to/related/parent-child→semantic. Tagged{origin: "beads"}for traceability.A-MEM enrichment: New beads docs get full
postIndexEnrich()— memory note construction, semantic/entity link generation, memory evolutionGraph traversal:
intent_searchandfind_causal_linkstraverse beads dependency edges alongside observation-inferred causal chainsRequirement:
bdbinary on PATH or at~/go/bin/bd
beads_sync MCP tool for manual sync; watcher handles routine operations automatically.
Fragment-Level Embedding
Documents are split into semantic fragments (sections, lists, code blocks, frontmatter, facts) and each fragment gets its own vector embedding. Full-doc embedding is preserved for broad-match queries.
Local Observer Agent
Uses the LLM server (shared with query expansion and intent classification) to extract structured observations from session transcripts. Observation types: decision, bugfix, feature, refactor, discovery, change, preference, milestone, problem. Each observation includes title, facts, narrative, concepts, and files read/modified. Preferences, milestones, and problems get first-class content_type treatment with dedicated confidence baselines and half-lives instead of being flattened to generic "observation". Regex patterns find decisions and antipatterns beside the model. A batch the model cannot answer (unavailable, or refusing the request) is quarantined and replayed later, never committed as empty; its regex finds are committed when the replay succeeds (v0.41.0). Each prompt is fitted in tokens to the LLM server's own context, with room kept for the reply, and a turn too long for one prompt runs as checkpointed windows that resume after a timeout (v0.41.2). Only the reply <none/> means "nothing to record"; a reply that does not parse gets up to two retries whose feedback names the failing field (a batch whose replies never parse is quarantined like an unanswered one), and on llama-server the observer asks for grammar-constrained replies — a GBNF grammar that admits only well-formed ones, sent unless the server refused one in the last 24 hours; enforcement is not verified in advance, and clawmem doctor counts completed replies that break it (v0.41.4). Serve the observer with -c 8192; inference services covers the server's context size.
Recall Tracking
Empirical tracking of which documents are surfaced by retrieval, which queries surfaced them, and whether the assistant actually cited them. Provides signals beyond raw search relevance for lifecycle decisions:
Per-query diversity: docs surfaced by multiple distinct queries have proven cross-domain relevance
Multi-day spacing: docs surfaced across separate calendar days (spaced frequency) are more valuable than binge recalls in one session
Negative signals: docs surfaced frequently but rarely referenced are noise candidates for snooze
Per-turn attribution: feedback-loop pairs each transcript turn with the context-surfacing row that served it (prompt hash + the host's order of events, never position) and tests only that turn's answer against that turn's injected documents (v0.41.0)
Data feeds lifecycle_status (pin/snooze candidate reports) and lifecycle_sweep (recall-based recommendations). Adapted from OpenClaw dreaming promotion patterns.
User Profile
Two-tier auto-curated profile extracted from your decisions and hub documents:
Static: persistent facts (Levenshtein-deduplicated)
Dynamic: recent session context
Injected at session start for instant personalization.
Prompt Injection Filtering
Five detection layers protect injected content: legacy string patterns, role injection regex, instruction override patterns, delimiter injection, and unicode obfuscation detection. Filtered results are skipped entirely (no placeholder tokens wasted).
Consolidation Worker
Optional background process that enriches documents missing A-MEM metadata. Runs on a configurable interval, processing 3 documents per tick. Non-blocking (Timer.unref).
Per-Folder CLAUDE.md Generation
Automatically generates context sections in per-folder CLAUDE.md files from recent decisions and session activity related to that directory.
Feedback Loop
A surfaced note gets access_count + 1 when a turn it was injected into names it — once per turn, when the turn is over (v0.41.0; earlier versions counted the whole session again at every Stop). Unreferenced notes decay via recency. Over time, useful notes rise and noise fades.
Offline Eval Harness
clawmem eval run --gold <file.jsonl> replays hand-labeled queries through the real query tool handler and scores the retrieved documents against gold evidence: doc-level Jaccard (|C∩E|/|C∪E|), precision/recall@k, hit@k, MRR. Touches no retrieval, lifecycle, or telemetry state (normal inference caches may populate, as in any live query); writes run.json + report.md for A/B comparison between checkouts or DB snapshots. Gold schema, trust gates, and labeling discipline: docs/guides/eval-harness.md.
Feature Flags
| Variable | Default | Effect |
|---|---|---|
CLAWMEM_ENABLE_AMEM | enabled | A-MEM note construction + link generation during indexing |
CLAWMEM_ENABLE_CONSOLIDATION | disabled | Background worker for backlog A-MEM enrichment |
CLAWMEM_CONSOLIDATION_INTERVAL | 300000 | Worker interval in ms (min 15000) |
CLAWMEM_EMBED_URL | http://localhost:8088 | Embedding server URL. Uses llama-server (GPU or CPU) or cloud API. Falls back to in-process node-llama-cpp if unset. |
CLAWMEM_EMBED_API_KEY | (none) | API key for cloud embedding. Enables cloud mode: batch embedding, provider-specific params, TPM-aware pacing. |
CLAWMEM_EMBED_MODEL | embedding | Model name for embedding requests. Override for cloud providers (e.g. jina-embeddings-v5-text-small). |
CLAWMEM_EMBED_TPM_LIMIT | 100000 | Tokens-per-minute limit for cloud embedding pacing. Match to your provider tier. |
CLAWMEM_EMBED_WAIT_SEC | 120 | v0.43.5: how long clawmem embed waits for a self-hosted embedding server at start (URL set, no API key, CLAWMEM_NO_LOCAL_MODELS=true). Not ready in time → the embedding is skipped, exit 1. embed --wait overrides it. |
CLAWMEM_EMBED_WAIT_INTERVAL_MS | 5000 | v0.43.5: time between that wait's probes. |
CLAWMEM_EMBED_PROBE_TIMEOUT_MS | 15000 | v0.43.5: how long one probe of that wait may take. |
CLAWMEM_EMBED_DIMENSIONS | (none) | Output dimensions for OpenAI text-embedding-3-* Matryoshka models (e.g. 512, 1024). |
CLAWMEM_LLM_URL | http://localhost:8089 | LLM server URL for intent/query/A-MEM and the Stop hooks' observer. Without it, falls to node-llama-cpp (if allowed). |
CLAWMEM_LLM_API_KEY | (none) | Bearer token for an authenticated remote LLM endpoint. Independent of the embed and rerank keys. |
CLAWMEM_LLM_MODEL | qwen3 | Model name sent to the configured LLM endpoint. Override this for OpenAI-compatible proxies such as gpt-5.4-mini. |
CLAWMEM_LLM_REASONING_EFFORT | (none) | Optional top-level reasoning_effort field for Chat Completions endpoints that support it (for example OpenAI reasoning models). Leave unset for llama-server/vLLM unless your serving stack explicitly accepts that field. |
CLAWMEM_LLM_NO_THINK | true | Append /no_think to remote LLM prompts. Set to false for standard OpenAI models and other endpoints that reject or treat the Qwen-style suffix as literal prompt text. |
CLAWMEM_LLM_CONTEXT_TOKENS | (none) | v0.41.2. The LLM server's context per request, in tokens, for a server without llama.cpp's /props (cloud gateways, vLLM, Ollama). The Stop hooks' observer and summary fit their prompts to it. With /props the server's own number wins; with neither, 4096 is assumed. |
CLAWMEM_OBSERVER_GRAMMAR | auto | v0.41.4. auto: the Stop hooks' observer sends a GBNF grammar that admits only well-formed observation blocks or <none/> to llama-server (a /props naming the model, template and build) and to the in-process model; off: never. A server that refuses one (HTTP 400, or in process a grammar that does not compile) gets none for 24 hours and until a grammarless request reaches it. Enforcement is not verified in advance: clawmem doctor counts completed replies that break the grammar. |
CLAWMEM_JUDGE_URL | (none) | v0.29.0. OpenAI-compatible endpoint for the contradiction judge — a task-scoped override, independent of CLAWMEM_LLM_* (query expansion stays on the stock model). Setting it activates contradiction analysis. Data-egress note: the judge receives new decisions + retrieved candidate snippets. Guide. |
CLAWMEM_JUDGE_PROVIDER | openai when _URL set | v0.29.0. openai | anthropic | claude-cli. anthropic calls the Messages API directly (default model claude-haiku-4-5); claude-cli runs a sandboxed headless claude -p on your Claude Code subscription — no API key. |
CLAWMEM_JUDGE_MODEL | provider-specific | v0.29.0. Wire model id. Required on openai (no universal default); defaults to claude-haiku-4-5 on anthropic/claude-cli (the suite-verified recommended default; claude-sonnet-5 is an unverified upgrade candidate — see the judge guide for its honest evaluation status). |
CLAWMEM_JUDGE_API_KEY | (none) | v0.29.0. Bearer / x-api-key for the judge endpoint. On anthropic, falls back to ANTHROPIC_API_KEY. |
CLAWMEM_JUDGE_NO_THINK | false | v0.29.0. Append /no_think on the judge lane (only useful for a local Qwen-family judge on the openai lane). |
CLAWMEM_JUDGE_STRUCTURED | lane-specific | v0.29.0. Schema-constrained judge output. Default false on openai (endpoint support varies), true on anthropic/claude-cli. Set explicitly to override. |
CLAWMEM_RERANK_URL | http://localhost:8090 | Reranker server URL. Without it, falls to node-llama-cpp (if allowed). |
CLAWMEM_RERANK_API_KEY | (none) | Bearer token for an authenticated remote reranker endpoint. Independent of the embed and LLM keys. |
CLAWMEM_NO_LOCAL_MODELS | false | Block node-llama-cpp from auto-downloading GGUF models. Set true for remote-only setups where you want fail-fast on unreachable endpoints. Since v0.43.5 clawmem embed first waits for a self-hosted embedding server under this setting (CLAWMEM_EMBED_WAIT_SEC). |
CLAWMEM_MCP_DIRECT_TUNED_WEIGHTS | (superseded) | No effect since v0.22.0 — the direct-pipeline eval this knob was gated on measured tuned weights at 1/19 hit@1; the direct vector routes rank by raw cosine instead. Still parsed for backward compatibility; setting it logs a once-per-process warning. |
CLAWMEM_MERGE_SCORE_NORMAL | 0.93 | v0.7.1. Phase 2 consolidation merge-safety threshold when candidate and existing anchors align. Merges above this normalized 3-gram cosine score are allowed. |
CLAWMEM_MERGE_SCORE_STRICT | 0.98 | v0.7.1. Strictest merge-safety threshold — fallback when anchor sets are ambiguous. |
CLAWMEM_MERGE_GUARD_DRY_RUN | false | v0.7.1. When true, Phase 2 merge-safety rejections are logged but not enforced — use for calibration before enabling the gate. |
CLAWMEM_CONTRADICTION_POLICY | link | v0.7.1, judge-gated v0.29.0. Merge-time contradiction gate policy. link inserts a new row and sets the old row's invalidated_by backlink (default). supersede marks the old row status='inactive' — requires a configured judge; without one it is loudly constrained to link and clawmem doctor reports the policy as inactive. |
CLAWMEM_CONTRADICTION_MIN_CONFIDENCE | 0.5 | v0.7.1. Minimum confidence required before the contradiction gate blocks a merge. The judge prompt states this same threshold — the prompt never overrides your configured value. |
CLAWMEM_CONTRADICTION_INVALIDATE | false | v0.28.0, judge-gated v0.29.0. A different subsystem from the two rows above — this one governs the decision-extractor hook, not the merge-time gate. Set to exactly true to let a contradiction that erodes a document to the 0.2 confidence floor also set invalidated_at, removing it from FTS and vector retrieval. Off by default: the hook logs WOULD invalidate, writes a durable audit row, and mutates nothing further. Confidence erosion runs whenever a judge is configured (without a judge, nothing runs at all). Eligible rows are content_type='observation' only. Calibration guide. |
Configuration
Collection Config
~/.config/clawmem/config.yaml:
collections:
notes:
path: /home/user/notes
pattern: "**/*.md"
autoEmbed: true
docs:
path: /home/user/docs
pattern: "**/*.md"
update: "git pull"
directoryContext: false # opt-in per-folder CLAUDE.md generationDatabase
~/.cache/clawmem/index.sqlite — single SQLite file with FTS5 + sqlite-vec extensions.
Frontmatter
YAML (or JSON) between --- lines that open the document, parsed through a bounded engine (v0.43.6 — a ---js block
used to run as code): only yaml/yml/json blocks are read; a block with a YAML anchor, deeper than 32 levels, over
4,096 nodes, over 64 KiB or over 1,024 lines is indexed as plain text; a << key does not merge (it is kept as an
ordinary key); tags keeps at most 64 scalar entries. Supported fields:
---
title: "Document Title"
tags: [tag1, tag2]
domain: "infrastructure"
workstream: "project-name"
content_type: "decision" # decision|preference|hub|research|project|handoff|conversation|progress|note
review_by: "2026-03-01"
---Suggested Memory Filesystem
This structure separates human-curated content from auto-generated memories, and within auto-generated content, separates user memories (persist across agents, owned by the human) from agent memories (operational, generated from sessions). Static knowledge lives in resources/ with no recency decay, distinct from ephemeral session logs. The layout works with any MCP-compatible client (Claude Code, OpenClaw, custom agents).
Workspace Collection
The primary workspace where the agent operates. Path varies by client (e.g., ~/workspace/, ~/.openclaw/workspace/, or any directory you choose).
<workspace>/ ← Collection: "workspace"
├── MEMORY.md # Human-curated long-term memory
├── memory/ # Session logs (daily entries)
│ ├── 2026-02-05.md
│ └── 2026-02-06.md
├── resources/ # Static knowledge — use content_type: hub (∞ half-life)
│ ├── runbooks/
│ │ └── deploy-checklist.md
│ └── onboarding.md
├── _clawmem/ # Auto-generated — DO NOT EDIT
│ ├── user/ # User memories (persist across agents/sessions)
│ │ ├── profile.md # Static facts + dynamic context
│ │ ├── preferences/ # Extracted preferences
│ │ └── entities/ # Named entities (people, services, repos)
│ └── agent/ # Agent memories (operational, session-derived)
│ ├── observations/ # Decisions + observations from transcripts
│ ├── handoffs/ # Session summaries with next steps
│ └── antipatterns/ # Accumulated negative patterns (∞ half-life)
└── ...Project Collections
Each project gets its own collection. Same structure, with optional Beads integration.
~/Projects/<project>/ ← Collection: "<project>"
├── .beads/ # Beads issue tracker (Dolt backend, auto-synced)
│ └── dolt/ # Dolt SQL database (source of truth)
├── MEMORY.md # Human-curated project memory
├── memory/ # Project session logs
│ └── 2026-02-06.md
├── resources/ # Static project knowledge (∞ half-life)
│ ├── architecture.md
│ └── api-reference.md
├── research/ # Research dumps (fragment-embedded, 90-day decay)
│ └── 2026-02-06-topic-slug.md
├── _clawmem/ # Auto-generated per-project
│ ├── user/
│ │ └── preferences/
│ └── agent/
│ ├── observations/
│ ├── handoffs/
│ ├── antipatterns/
│ └── beads/ # Beads issues as searchable markdown
├── CLAUDE.md
├── src/
└── README.mdDesign Principles
| Principle | Rationale | ClawMem Mechanism |
|---|---|---|
| User/agent separation | User memories (preferences, entities, profile) are owned by the human and persist indefinitely. Agent memories (observations, handoffs) are operational artifacts with lifecycle management. | _clawmem/user/ vs _clawmem/agent/ — different decay rules per content type |
| Resources are first-class | Static knowledge (runbooks, architecture docs, API refs) should never lose relevance due to recency decay. | resources/ indexed with content_type: hub → ∞ half-life in composite scoring |
| Progressive disclosure | Hook injection (2000 token budget) benefits from tiered loading: compact snippets first, full content on demand. | compact=true (L1) → multi_get (L2) at query time. Pre-computed abstracts not yet implemented — candidate for future L0 tier. |
| Beads as memory edges | Issue tracker data bridges into the knowledge graph via typed relations, not just as flat documents. | syncBeadsIssues() maps deps → memory_relations: blocks→causal, discovered-from→supporting, relates-to→semantic |
| Each turn captured once, per session | Session-derived memory must neither repeat itself at every response nor overwrite another session's. | Transcript cursors for extraction and handoffs, a once-per-turn feedback verdict, items rendered into per-session decision/antipattern/handoff docs (v0.41.0; the cross-session merge policies and hash-window dedup they replaced are gone) |
Layer Mapping
| Layer | Path | Owner | Decay | ClawMem Role |
|---|---|---|---|---|
| Long-Term Memory | MEMORY.md | Human | ∞ | Indexed, profile supplements, human-curated anchor |
| Session Logs | memory/*.md | Human | 60 days | Indexed, daily entries, handoff auto-generated |
| Static Resources | resources/**/*.md | Human | ∞ (hub) | Fragment-embedded, no recency penalty |
| Research | research/*.md | Human | 90 days | Fragment-embedded for granular retrieval |
| User Profile | _clawmem/user/profile.md | Auto | ∞ | Static facts + dynamic context |
| User Preferences | _clawmem/user/preferences/*.md | Auto | ∞ | Extracted preferences |
| User Entities | _clawmem/user/entities/*.md | Auto | ∞ | Named entities across sessions |
| Observations | _clawmem/agent/observations/*.md | Auto | 180d (decision) | Decisions + observations from transcripts |
| Handoffs | _clawmem/agent/handoffs/*.md | Auto | 30 days | Session summaries with next steps |
| Antipatterns | _clawmem/agent/antipatterns/*.md | Auto | ∞ | Accumulated negative patterns |
| Beads | _clawmem/agent/beads/*.md | Auto | ∞ | Beads issues synced from Dolt, relations in memory graph |
Manual layers benefit from periodic re-indexing — a cron job running clawmem update --embed keeps the index fresh for content edited outside of watched directories.
Setup
# Bootstrap a content directory (creates vault + indexes + embeds + installs hooks + MCP)
clawmem bootstrap ~/notes --name notes
# Bootstrap each project you want indexed
clawmem bootstrap ~/Projects/my-project --name my-project
# Install watcher + embed timer as systemd services
clawmem install-service --enableOpenClaw-specific
# Install the OpenClaw plugin
# v0.10.4+: delegates to `openclaw plugins install --force` when the CLI is on PATH (auto-enables;
# honors OPENCLAW_STATE_DIR / OPENCLAW_CONFIG_PATH / --profile). Falls back to recursive copy
# honoring OPENCLAW_STATE_DIR when the CLI is absent.
# v0.10.0–v0.10.3: hardcoded recursive copy into ~/.openclaw/extensions/clawmem (issue #11).
clawmem setup openclaw
# Then follow the printed next steps. The exact list depends on which path ran:
# Path 1 (delegated): plugin already auto-enabled — just `openclaw gateway restart` + GPU config
# Path 3 (CLI absent): manually `openclaw plugins enable clawmem`, then restart + GPU config
# Multi-user installs also need: sudo chown -R <gateway-user>:<gateway-group> <extensions>/clawmem
# Requires OpenClaw v2026.4.11+.Index your content directories with clawmem bootstrap as above. The OpenClaw plugin shares the same vault as Claude Code hooks.
Hermes-specific
# Install the memory provider plugin.
# Preferred: $HERMES_HOME/plugins/clawmem/ (user-plugin path, Hermes #10529, v2026.4.13+).
ln -s $(npm root -g)/clawmem/src/hermes ${HERMES_HOME:-~/.hermes}/plugins/clawmem
# Bundled-style path is also supported (takes precedence on name collisions):
# ln -s $(npm root -g)/clawmem/src/hermes /path/to/hermes-agent/plugins/memory/clawmem
# Start the REST API (required for Hermes tool calls)
clawmem serve --port 7438 &
# Configure Hermes to use ClawMem (in your Hermes config.yaml):
# memory:
# provider: clawmemDependencies
| Package | Purpose |
|---|---|
@modelcontextprotocol/sdk | MCP server |
gray-matter | frontmatter delimiting (pinned 4.0.3; engines replaced by ClawMem's bounded YAML/JSON, see v0.43.6) |
js-yaml | the YAML loader under ClawMem's bounded frontmatter engine (3.14.x) |
node-llama-cpp | GGUF model inference (reranking, query expansion, A-MEM) |
sqlite-vec | Vector similarity extension |
yaml | Config parsing |
zod | MCP schema validation |
Deployment
Three-tier retrieval architecture: infrastructure (watcher + embed timer) → hooks (~90%) → agent MCP (~10%). Works out of the box without a dedicated GPU (all models auto-download via node-llama-cpp, uses Metal on Apple Silicon). For best performance, run the inference services on GPU (three llama-server instances in either stack; the SOTA stack's zerank-2 reranker runs as a Q8_0 GGUF that carries its score head, with a bf16 transformers sidecar as the reference alternative) — see Inference services for model tiers (SOTA vs QMD native) and cloud embedding for cloud embedding alternatives.
Key services: clawmem-watcher (auto-index on file change + beads sync), clawmem-embed timer (daily embedding sweep), 7 Claude Code hooks installed by default (context injection, curator nudge, compaction support, decision extraction, handoffs, feedback). Optional clawmem-curator agent for on-demand lifecycle triage, retrieval health checks, and maintenance (clawmem setup curator).
Acknowledgments
Built on the shoulders of:
A-MEM — self-evolving memory architecture
Beads — Dolt-backed issue tracker for AI agents
claude-mem — Claude Code memory integration reference
Engram — observation dedup window, topic-key upsert pattern, temporal timeline navigation, duplicate metadata scoring signals
Hermes Agent — MemoryProvider plugin integration, memory nudge system (periodic lifecycle tool prompting)
Hindsight — entity resolution, MPFP graph traversal, temporal extraction, 3-tier consolidation, observation invalidation, 4-way parallel retrieval
Honcho — deductive observation synthesis patterns, surprisal-based anomaly scoring concept, embed-state self-healing, retrieval separation (raw vs derived)
MAGMA — multi-graph memory agent
MemPalace — conversation import patterns, broadened observation taxonomy (preference/milestone/problem), session-bootstrap synthesis
memory-lancedb-pro — retrieval gate, length normalization, MMR diversity, access reinforcement algorithms
OpenClaw — recall tracking patterns (per-query diversity, multi-day spacing, negative signal tracking, promotion scoring) extracted from the dreaming memory consolidation system
OpenViking — query decomposition patterns, collection-scoped retrieval, transaction-safe indexing
QMD — search backend (BM25 + vectors + RRF + reranking)
SAME — agent memory concepts (recency decay, confidence scoring, session tracking)
supermemory — hook patterns and context surfacing ideas
Thoth — anti-contamination deductive synthesis, contradiction-aware + name-aware merge gates, post-import conversation fact extraction, quiet-window heavy maintenance lane with worker leases, context instruction framing, relationship snippets, multi-turn prior-query lookback
Roadmap
| Status | Feature | Description |
|---|---|---|
| :white_check_mark: | Relevance admission | Distribution-relative admission on the fused ordering key with query-level abstention — replaced the v0.1.3 adaptive composite thresholds (v0.38.0) |
| :white_check_mark: | Deep escalation | Budget-aware query expansion + cross-encoder reranking for deep profile |
| :white_check_mark: | Cloud embedding providers | Jina, OpenAI, Voyage, Cohere with batch embedding + TPM pacing |
| :construction: | Calibration probes | One-time clawmem calibrate command that measures your vault's score distribution and tunes thresholds automatically |
| :white_check_mark: | Judged hook eval harness | clawmem eval hook-run — labeled replay of the real hook with paired A/B counterfactuals, replicated draws, and acceptance gates (v0.38.0) |
| :memo: | Multi-vault namespacing | Isolated vaults with independent calibration and lifecycle policies |
| :memo: | REST API authentication | Token-scoped access for multi-agent deployments |
| :white_check_mark: | Budget-bounded rerank lane | Deep-profile cross-encoder rerank inside the hook budget with full-coverage + degeneracy contracts (v0.38.0) |
:white_check_mark: Shipped  :construction: Planned  :memo: Exploring
License
MIT
RELEASE_NOTES.md
ClawMem — Release Notes
For upgrade instructions (migration steps, opt-in features, verification commands), see docs/guides/upgrading.md. This file is the chronological feature record, newest first.
v0.43.6 — frontmatter can no longer run code or blow up memory while it is indexed
Two defects in frontmatter parsing, both reachable from any Markdown file ClawMem indexes, plus one unbounded cache.
Code execution. gray-matter, the frontmatter library, reads the text after the opening --- as a language name
and runs the matching engine. Its default table includes a javascript engine that evaluates the block as code, and
ClawMem passed no options, so a file whose first line was ---js (or ---javascript, any letter case, with a CRLF
line ending or a byte-order mark before it) ran its frontmatter as JavaScript inside the indexing process: clawmem update, the watcher, the MCP reindex and vault_sync tools, the REST /reindex, clawmem embed (which re-parses
every stored document) and the embedding canary. Anything that process could do, the file could do.
Memory amplification. The default YAML engine accepted aliases, << merge keys and !!omap, so a small block
could expand to an object many orders of magnitude larger (an alias tree six levels deep, a mapping merged into itself
128 times, an array key of 128 aliases to a 1 KiB string), and gray-matter's comment stripper is quadratic in runs of
blank lines. Unbounded cache. gray-matter caches every parsed text in a module-level object for the life of the
process; the watcher and the MCP servers retained every file they ever parsed, and a repeated malformed parse was
served from that cache.
What changed
One frozen options object on the one call (
src/indexer.ts,FRONTMATTER_OPTIONS). Thejavascriptandcoffeeengines refuse; after the call the language must beyaml/yml(any case) or exactlyjson, else the file is read as plain text. gray-matter's excerpt step is off. Passing options also bypasses gray-matter's cache, so nothing is retained between parses and no entry can be served to a later file.A bounded YAML engine of ClawMem's own (
makeFrontmatterYamlEngine). Built on js-yaml's safe schema minus the merge and omap types; it refuses any anchor (so no alias can be read), stops at 32 levels of nesting and 4,096 nodes, and takes at most 64 KiB. A refused block is read as plain text — the same path a malformed block took before.The block is measured before gray-matter sees it. A frontmatter block over 64 KiB or over 1,024 lines (any line ending) never reaches the library; the file is plain text. Worst case for a crafted note at the caps is about 0.1 s of indexing.
The guard checks itself. At its first parse in a process the engine runs a self-test of the contracts it relies on (the YAML loader reports every node, in order, with its depth, and exposes anchors). If the installed loader does not, the guard marks itself broken: every YAML frontmatter block then reads as plain text, one line goes to stderr, and
clawmem doctorprints a ✗ line. Fail closed, never open.Bounded tags.
tagskeeps string, number and boolean elements only, each up to 256 characters, at most 64; nested lists and objects are dropped (they used to become joined strings or[object Object]).Pins.
gray-matteris pinned exactly to 4.0.3 (unmaintained since 2021; the fix relies on its engine lookup) andjs-yamlbecomes a direct dependency at~3.14.2.
What this changes for your vault
A file whose first line is
---followed by a language other than yaml/yml/json (---js,---toml,---coffee) is indexed as plain text: no title, no tags, the whole file is the body. Before,---jsran the block;---tomlalready read as plain text.Frontmatter that defines a YAML anchor (
&name), uses!!omap, nests deeper than 32 levels, has more than 4,096 nodes, or exceeds 64 KiB or 1,024 lines, is read as plain text. A<<key no longer merges; it is an ordinary key. Measured over 3,617 real frontmatter blocks across two vaults: none used any of these.excerpt_separatorin a document has no effect (ClawMem never read the excerpt).tagsentries that were nested lists or objects are dropped; long entries (over 256 characters) are dropped; a list over 64 entries is cut to its first 64.No schema, config, CLI or MCP change. The memory the watcher and MCP servers held for parsed texts is gone.
Upgrading
Restart every long-lived ClawMem process after upgrading: the fix lives in src/indexer.ts, which clawmem watch,
every clawmem mcp server and clawmem serve load once at start and keep. One-shot commands (update, embed,
mine, curate, doctor) pick up the fix on their next run. Find the long-lived ones with
ps -eo pid,lstart,args | grep -E 'clawmem (watch|mcp|serve)'. Hermes discards its child's stderr, so a broken guard
shows there only through clawmem doctor. See
upgrading.
Known limits
YAML 1.1 base-60 numbers (
1:30→ 90) are assembled with a quadratic step inside js-yaml; within the 64 KiB cap that is about 0.1 s. Kept, since removing base-60 would change stored metadata.The self-test narrows what a js-yaml patch release can silently change to the contracts it checks; a patch that changed behaviour outside them (timestamp resolution, say) is caught only by the ordinary metadata tests.
The guard protects ClawMem's own parse. Another program that reads the same files with gray-matter's defaults is not protected by this release.
Verification
New tests, written first and run on v0.43.5's code before the change: tests/unit/frontmatter-engines.test.ts, 79 cases
on Bun 1.3.14 and 1.4.2 — 53 failed on v0.43.5 (every exec spelling ran its payload; the alias, merge, omap and
array-key fixtures parsed; the cache retained and served entries; the over-cap blocks reached the library; the pins and
the self-check did not exist) and 26 guards passed, with identical results on both Bun versions. The tests drive the
real clawmem embed and clawmem doctor as child processes; a positive control shows the same payload writing its
marker through an unprotected parse. A source guard reads every shipped .ts/.js file with the TypeScript compiler's
parser and fails unless gray-matter is bound once, by its default import, and called once, with the guarded options,
and js-yaml's binding reaches only the engine builder — so the protection cannot be bypassed by a later edit without
a test failing. Parity on real data, read-only: the old and the new parser return identical
{body, meta} for every one of 40,335 stored documents across two vaults (32,710 with frontmatter) and for the raw file
of every one of 4,886 file-backed documents.
Full suite: 3,915 pass / 0 fail across 196 files on Bun 1.3.14, and the same on Bun 1.4.2. tsc reports the same 85 errors as on v0.43.5, none in the files this release touches.
The cross-model adversarial pass (codex) reviewed the design over seven turns to zero findings, and the code over four turns in a separate session to zero findings. All nine code findings were Low and all were in the test file: six on the first turn (a source guard that read the source with a regex, a test fixture that duplicated another after BOM stripping, line-count fixtures one line over, a missing JSON control, two documentation lines), then one, then two more in the test's hand-written tokenizer, which the final turn replaced with the TypeScript compiler's parser. No finding touched the shipped code.
v0.43.5 — clawmem embed waits for a self-hosted server that is not up yet, and its failed stops exit 1
A scheduled clawmem embed that started while its self-hosted embedding server was still down, with the in-process
fallback off, put the server into the 60 s cooldown on its first failed request. Every later request failed at once,
the run charged every pending document an attempt, and it wrote no vector. After three such runs in a row a document was
skipped until its content changed. Several stops also exited 0, so a scheduler saw nothing wrong.
What changed
Startup readiness wait (
src/embed-readiness.tsnew,src/llm.ts,src/clawmem.ts). WhenCLAWMEM_EMBED_URLis set,CLAWMEM_EMBED_API_KEYis not, andCLAWMEM_NO_LOCAL_MODELS=true,clawmem embedprobes the server before the geometry canary, the stale-vector cleanup and any document attempt: at once, then every 5 s, for up to 120 s (embed --wait <seconds>,CLAWMEM_EMBED_WAIT_SEC,CLAWMEM_EMBED_WAIT_INTERVAL_MS,CLAWMEM_EMBED_PROBE_TIMEOUT_MS;--wait 0sends one probe). One deadline bounds every probe and every pause. When the server answers an embedding, the cooldown and the HTTP-error count are cleared and the run goes on as before. When it does not, the run printsEmbed server <url> did not become ready within <N> s (last: <kind>[, HTTP <status>]), skips the embedding, and exits 1, with no document charged and no stale vector removed. A server that answers HTTP 405 or 501 (another service on the port) stops the wait at once. Underupdate --embedthe indexing has finished by then; only the embedding is skipped. The probe (LlamaCpp.probeRemoteEmbed) sends one request and changes no state of its own;LlamaCpp.markRemoteEmbedReadydoes the clearing.Every other setup is unchanged in its routing: local fallback allowed (the stock launcher's default), a cloud key (including a self-hosted server that takes a key), or no URL. The env knobs are read only where the wait applies, so a mistyped one cannot stop those runs;
--waitis checked on every run.The vault's stored model is bound on every non-force run. On a vault with one stored model, every vector is now checked against it. Through v0.43.4 the model was bound only from a start probe that answered, so when that probe failed, a vector of the same dimension from another model was stored: from the in-process fallback during a cooldown, or from a server that came back serving another model. Such a run now stops with
Embedding model changed mid-runand stores nothing of the other model.--forcestill binds the model the server reports, so migrations and repairs by--forcework as before.Exit codes. The stops for an embedding dimension change, a model change, a vault with mixed models and a
--forcerun that cannot reach the endpoint now exit 1. A run that scheduled documents and wrote no vector printsNo documents were embedded: N scheduled, 0 vectors written.and exits 1, whatever its geometry checks said. A run with nothing to embed whose end check passes still exits 0.Tests:
tests/unit/embed-cooldown-cli.test.tsandtests/unit/embed-cooldown-probe.test.ts(new), with a shared fixture intests/helpers/embed-readiness-fixture.ts: a scripted/v1/embeddingsserver and a TCP front that resets chosen requests. Three existing tests used a closed port, or a server that refused every non-canary request, with the fallback off; they now model a server that answers its first request and then goes away, or answer the readiness probe, so they keep testing what they tested:tests/unit/embed-taint-v0412.test.ts(two cases) andtests/unit/canary-validation.test.ts(the no-work case).Docs:
docs/reference/cli.md,docs/reference/configuration.md,docs/troubleshooting.md,docs/guides/systemd-services.md,docs/guides/inference-services.md,docs/guides/upgrading.md.
Upgrading
Nothing migrates. A systemd OnFailure= hook or a set -e script around clawmem embed now fires or stops on the
exits listed above. A server that needs more than 120 s to start: set CLAWMEM_EMBED_WAIT_SEC. See
upgrading.
Known limits
A server that goes away after the wait behaves as before. Before the canary, a non-force run warns and goes on, and a
--forcerun refuses at the canary. During the sweep, the remaining requests fail at once for 60 s, and each document whose fragments fail is charged an attempt (a document that completes resets its count). Three such runs in a row still skip a document without a report. Recovery mid-run and the report are BACKLOG 77.3 and 77.4.With the in-process fallback allowed, a server that is down still meets the fallback; the new model check refuses its vectors only where they differ from a vault's one stored model. A fresh vault, and a vault whose stored model is the in-process one, still embed locally. One backend per run is BACKLOG 77.2.
A cloud setup can still fall back to the in-process model after a transport error (BACKLOG 77.2).
Verification
New tests, written first and run on v0.43.4's code before the change: in embed-cooldown-cli.test.ts, 10 cases failed
there on the fields the change is about (attempts charged, stale vector removed, the readiness message, the exit code,
the other model stored), and 2 guards passed. Each case that scripts a server failure (a reset, an HTTP status) also
checks that the failure happened, and those checks held on both versions. The same held on Bun 1.3.14 and 1.4.2. The 19
cases of embed-cooldown-probe.test.ts cover the new functions and could not run on v0.43.4. The three changed
existing tests pass on v0.43.4 and on this release.
Full suite: 3,838 pass / 0 fail across 195 files (3 todo) on Bun 1.3.14 and the same on Bun 1.4.2. tsc reports the same 85 errors as on v0.43.4, none in the files this release adds; the O1 seam audit passes.
The cross-model adversarial pass (codex) reviewed the design over five turns (11, 9, 4, 1 and 0 findings), which narrowed it to this release and moved the rest to BACKLOG 77.2–77.4. Its first code turn found 3: the wait accepted a ready answer after the embed lease was lost during the probe, the test server's TCP front counted requests by the first bytes of a socket read (a request line split across reads went uncounted), and four docs said the wait came before anything the run does. Each code fix got a test that failed on the code before it. The second code turn cleared the fixes with no finding. Two more turns cleared these notes and the commit message after one finding: the commit message credited all three code-review findings with a test that failed first, where one of them was a docs fix.
v0.43.4 — clawmem repair entities moves the entity links made before v0.43.2's resolver fix
v0.43.2 stopped the entity resolver from joining names whose numbers differ (Node 200 into Node 202) and made an
entity with the exact name win over fuzzy candidates. The links written before then stay as they were: a mention or a
triple endpoint attached to the wrong entity keeps it, because nothing resolves them again. An unchanged document's
entities are never extracted again, and triples are never resolved again.
What changed
New command:
clawmem repair entities(src/entity-repair.ts,src/clawmem.ts). It moves a mention or a triple endpoint only when its own stored text proves the link wrong under the fixed rule: the text's numbers differ from the linked entity's name's (R1), or the text names another entity in the same bucket exactly (R2). It resolves those links again, one at a time in the order they were seen, with the real resolver, on an in-memory copy of the vault's entity tables, and creates an entity where today's resolver would. Same-name duplicates (R3) and other differences, such as types assigned inconsistently (R4), are counted and left alone. Links whose case is ambiguous are held and listed with their reason: a predicate that occurs twice in the fact, a name of another bucket, a match through another name's search row, an ID taken by another name, among others.Triples are regrouped by their repaired key: a triple keeps its id when its key stays, merges into an untouched triple with the same key, is re-pointed, or splits; equal evidence rows collapse to the earliest.
Mentions move within each document; two that meet at one entity collapse to one.
mention_countand the co-occurrence counts change by the difference.Entity edges: an edge is added only where the repair makes two documents share an entity that clears enrichment's specificity threshold and nothing they shared before does. Existing edges are never changed or removed; those whose recorded entity the two documents no longer share are listed as stale.
Dry run by default, on a read-only handle in one read snapshot: it prints every planned change and a plan digest.
--apply --vault <name>plans again under the write lock; with--expect <digest>it writes only when the new plan's digest is the one given (edge weights are not in the digest), and without it, whatever it plans then. It checks every answer and the added edges against the vault before committing, and logs every change inentity_repair_log.--restore <op> --vault <name>reverses each group of changes whose logged triples and evidence rows, or whose document's whole set of mentions, are still as the apply left them, unless undoing it would put a triple back beside an open triple that took its subject, predicate and object meanwhile. Counts are reversed by their difference; added edges, and created entities still in use, stay. The command opens vault files directly and never initializes a store. It lists each database file once: another configured name for a file already listed selects that vault, and when a configured vault on another file is namedgeneral, the default index is listed asdefault-index(ordefault-index-2and up, when that name is configured for another file too).
Enrichment's edge step is a helper (
computeEntityEdgesandwriteEntityEdgeinsrc/entity.ts), which the repair shares; enrichment writes the same edges as before. Seven resolver helpers are exported. No behaviour changes.Tests:
tests/unit/entity-repair.test.tsandtests/unit/entity-repair-cli.test.ts(new).Docs:
docs/internals/entity-resolution.md(Repairing links made before v0.43.2),docs/reference/cli.md,docs/guides/upgrading.md,docs/troubleshooting.md.
Upgrading
No action needed: nothing migrates, and the command changes a vault only with --apply or --restore. To repair a
vault, run the dry run, review it, then apply each vault with --expect and the digest it printed; keep the op id for
--restore. See
upgrading.
Verification
88 new tests: 82 in tests/unit/entity-repair.test.ts, 6 in tests/unit/entity-repair-cli.test.ts. The command was
written before its tests. The changes for the review below were written test-first: each change's new tests ran on the
code before it, where those aimed at the change failed and some that guard kept behaviour passed. For the first turn's
findings, which assertion each test failed on was checked afterwards, on a copy of the earlier code rebuilt from its
text; there a restore-order test failed inside apply, and in restore once the old restore ran after the fixed apply.
Full suite on Bun 1.3.14: 3,802 pass / 5 fail across 193 files (3 todo). All 5 failures are in
tests/integration/cmdwatch-workers.integration.test.ts, which this release does not change: each test hit Bun's 5 s
default before its helper, which allows the watcher up to 8 s to print anything, had finished; those five tests set no
timeout of their own (the file's sixth allows 90 s). Run alone, the file passes 6 of 6, twice with this release and once
on v0.43.3, and v0.43.3's full run on Bun 1.3.14 (below) passed it. On Bun 1.4.2: 3,806 pass / 1 fail across 193 files
(3 todo). The failure is tests/integration/store-concurrent-init.test.ts with database is locked, the
timing-dependent failure noted under v0.43.3; run alone 15 times, it failed once with this release and once on v0.43.3.
tsc unchanged.
On a copy of a working vault (3.44 GB), the dry run plans to move 41 links under R1 and 3 under R2, holds 2 (a name of
another bucket, a predicate that occurs twice) and counts 112 same-name duplicates and 150 other differences. Apply
creates 34 entities, updates 31 triples and inserts 2, moves 3 evidence rows and 9 mentions, adds no edge and lists 14
as stale. A second process that asked for the write lock every 5 ms without waiting found it taken for one stretch of
841 ms during apply (1.04 s in all) and of 203 ms during restore (0.43 s); both are shorter than the default 5 s
busy_timeout. On an in-memory copy of the same vault, apply makes the same writes and leaves no foreign-key violation,
no triple without evidence and no open key held twice; planning again finds nothing to move and 2 holds; restore
reverses all 42 groups and deletes the 34 entities, and the entities, their search rows, the mentions, the triples and
their evidence, the co-occurrences and the document links then compare equal to before (entities by id, type, name,
mention count, canonical id and vault; mentions by content, since a moved mention gets a new row id). That comparison
was made on the in-memory copy, not on the disk copy.
The cross-model adversarial pass (codex) reviewed the design over nine turns, and cleared the code at its third turn,
after 9 findings (8, then 1): evidence rows of two moved triples could collide on the evidence table's unique index, in
apply and in restore; restore could reopen a fact closed after the apply, because a carrying triple the repair left
unchanged was not logged in its final state; the command read general as the default index even where a configured
vault has that name; after apply removed and re-created a co-occurrence pair, restore left it with the apply's time, and
after the first fix still did when the pair's stored time was NULL; the plan digest left out the order times and the
original endpoints; the alias check trimmed a name that the resolver compares untrimmed; the report lacked the endpoints
and the held count; and three tests could pass on wrong code. The first turn also confirmed two gaps named to it: cases
of the design with no test, and a restore that could leave two open triples with one key (restore now keeps such a
group). On the review's advice, apply holds a group that would give a key a second open triple (open-key-taken). Four
more turns cleared these notes, the docs and the commit message, after 11 findings: 9 closed at the second turn, 1 at
the third and the last at the fourth.
What didn't change
The resolver, its buckets and thresholds, enrichment and the edges it writes, and every other command. No existing
table changes; the first --apply creates entity_repair_log. The repair never deletes an edge, never merges
same-name duplicates (R3), and never moves a link that its own stored text does not prove wrong: a merge of two names
whose numbers agree stays unless the text names another entity in the bucket exactly.
v0.43.3 — an index pass waits for the write lock instead of failing with database is locked
indexCollection (src/indexer.ts) runs each index pass in one transaction, opened with a plain BEGIN. SQLite starts
such a transaction deferred: the pass read the stored rows and content hashes first and asked for the write lock only at
its first write, for a new or changed file. SQLite does not call the busy handler when a connection that already holds
a read transaction asks for the write lock. So when another process was writing to the vault (a hook, the MCP server,
clawmem embed), or had committed since the pass began reading, the pass's first write failed at once with database is locked, and busy_timeout never applied. The pass rolled back whole. The watcher logged Watch error: database is locked and dropped the change, so the vault kept the file's previous version until the file changed again or clawmem update ran. clawmem update, clawmem reindex, clawmem mine, the MCP tools reindex and vault_sync, and the REST
server's POST /reindex run the same pass and could fail the same way.
What changed
The pass takes the write lock first (
indexCollection,src/indexer.ts): it opens withBEGIN IMMEDIATE, so while another connection writes, it waits up tobusy_timeout(5 s by default) like any other writer, then reads, writes and commits as before. A-MEM enrichment and its model calls still run after the commit.The cost: the pass holds the write lock for its whole length, also when no file changed, because it reads and hashes the collection's files under the lock; other writers wait for it within their
busy_timeout. Measured with a copy of a vault on a RAM disk (models off): 42–47 ms for a collection of 815 unchanged files, 209 ms with 16 new ones; slower storage, or pages not yet in memory, lengthen it. While the watcher's pass waits for another writer's lock, the watcher answers no vector searches, and a hook whose vector search runs out of time falls back to keyword search for that prompt.Tests:
tests/unit/indexer-write-lock.test.ts(new). A second process holds the vault's write lock for 0.5 s while a pass indexes a new file: the pass waits and indexes it. A pass with nothing to write completes under the same contention.Docs:
docs/troubleshooting.md(Indexing),docs/guides/upgrading.md.
Upgrading
No action needed beyond upgrading every process that indexes the vault: restart clawmem watch and any running clawmem serve, and reconnect open agent sessions so that their MCP servers' reindex and vault_sync run the new code.
Nothing migrates. If the watcher logged
Watch error: database is locked before the upgrade, run clawmem update once to index anything a failed pass left
behind. See upgrading.
Verification
The new test runs a second process that holds the vault's write lock for 0.5 s while a pass runs, and times the pass's
own BEGIN: an attempt counts only when that BEGIN was called while the lock was held, returned after the holder let
go, and blocked for at least 20 ms (up to three attempts per case). On v0.43.2 the new-file case fails with
SQLITE_BUSY at the pass's first write. The case with nothing to write fails on v0.43.2 too, but not because of the
bug: its pass took no write lock there, so it had nothing to wait for; the case pins the new waiting behaviour. With the
fix both pass, in five consecutive runs. Full suite on Bun 1.3.14: 3,719 pass / 0 fail across 191 files (3 todo). On
Bun 1.4.2, of three full runs, the first two each failed one test, tests/integration/store-concurrent-init.test.ts
(three processes initializing one new vault file at once), a timing-dependent failure seen before this release
(3,718 pass / 1 fail), and the third passed (3,719 / 0); three full runs of v0.43.2 passed (3,717 / 0). None of the 21
files that test loads changed; three tests that run before it in the same run index through the change, each
on its own vault. In nine alternating runs of
tests/integration per version (where that test runs, in the same order as in the full suite), with other processes
held under one CPU core while it ran, it failed once on each version; a tenth v0.43.2 run, made while other work loaded
the machine, was set aside (it passed). Run alone, the test failed 1 time in 25 here and 0 times in 100 on v0.43.2.
tsc unchanged. The cross-model adversarial pass (codex) cleared the release at its third turn, after 3 findings, all in
the test: an elapsed-time assertion that could fail on correct code, a holder process that could escape cleanup, then
an overlap check that could accept a run without contention at the pass's BEGIN.
What didn't change
What a pass reads and writes, its rollback on any error, enrichment after the commit, the busy_timeout values, and the
schema. A pass still fails when another process holds the write lock for longer than busy_timeout, now after the wait,
and so can a pass with nothing to write, which before took no write lock and never waited.
v0.43.2 — names that differ only in a number stay separate entities, and kg_query returns the named entity's facts
The entity resolver joins a new name to an existing entity when the two are similar enough: a Levenshtein ratio of 0.75
(0.65 for people). One changed digit costs a short name little — node 200 and node 202 score 0.875 — so a new host,
port, version or date merged into whichever entity carrying a neighbouring number existed first, and what was later
said about it landed there too: document mentions, and the subjects and objects of the triples decision-extractor
stores. An exact name could lose as well: the FTS5 lookup that gathers the fuzzy candidates applies its 20-row limit
before any ranking, so where many names share a word (node in every Node …) the entity carrying exactly the new name
could be missing from the pool. The prompt hook resolves the proper nouns it finds through the same resolver, so a
prompt naming Node 200 could surface the facts of Node 202.
kg_query resolved its argument with a word search over entity IDs, names and types, ranked by mention count and cut to
one row. A name could answer with a more-mentioned entity that shared one word with it (Node 200 with Node 202). A
canonical ID fared worse: default and project in default:project:node_200 occur in the ID or type of almost every
entity, so an ID lookup typically answered with the entity mentioned most in the whole vault.
What changed
An exact name wins first (
resolveEntityCanonical,src/entity.ts). An entity in the same bucket whose name equals the new one, ignoring letter case and leading or trailing spaces, is the match before any fuzzy step; among several, the oldest wins, the row the old lookup in practice chose. Case is folded as JavaScript lowercases it, and SQLite'sLOWER()folds ASCII letters only: a name that is ASCII once trimmed and lowercased and holds a letter or digit is found through theLOWER(name)index plus one FTS5 phrase lookup, and any other name (İstanbul,ⓐⓑ,+++) is compared with every entity name in JavaScript, since FTS5 splits or drops such characters.A fuzzy match keeps its numbers. A candidate must hold the same runs of digits, in the same order, as the new name. A variant that adds or drops a number (
Driver 590vsDriver 590.44) now becomes its own entity: the rule can split one thing in two, but never joins two.kg_queryresolves the most specific way first (resolveEntityQueryinsrc/entity.ts,src/mcp.ts): an existing canonical ID; then every entity with exactly the argument's name, each listed under its own ID when several share it; then a search of entity names only, by whole word and then by word start, most-mentioned first, whose answer must contain the argument's numbers in order. An argument shaped like a canonical ID is never name-searched: when no entity has that ID, the argument itself is the lookup key, and any facts recorded under it are returned. The text header names the resolved entity ID and, after a name search, says which entity was chosen and how;structuredContentgainsresolution(viaand the resolvedentities) and a per-factentityId. The tool's description says that an exact name wins and that entities sharing a name are listed separately.Tests:
tests/unit/entity.test.ts,tests/unit/kg-query.test.ts(new),tests/unit/spo-extraction.test.ts,tests/unit/vault-facts.test.ts.Docs:
docs/reference/mcp-tools.md(kg_query),docs/internals/entity-resolution.md,docs/internals/graph-traversal.md,docs/guides/upgrading.md,docs/troubleshooting.md,README.md. Corrected on the way:reindex --enrichwas described as re-extracting every document's entities, but extraction skips a document whose title and body are unchanged since its last extraction, so--enrichbackfills new or changed documents only (docs/internals/entity-resolution.md§ Enrichment lifecycle,README.md,docs/reference/cli.md,docs/guides/inference-services.md,docs/guides/upgrading.md,docs/troubleshooting.md).
Upgrading
No action needed beyond upgrading every process that shares the vault: restart clawmem watch and reconnect open agent
sessions so that their MCP servers run the new kg_query. Nothing migrates. Mentions and triples that an earlier
version attached to the wrong entity keep it; no repair command ships yet. A client that parses kg_query's header line
needs updating. See upgrading.
Verification
The fix adds 34 tests, all run on v0.43.1 with the new resolveEntityQuery stubbed to throw so that the files load: 31
fail and 3 pass. 7 fail on the entity that the resolver, the triple path or the prompt hook picks, and 3 pass, guarding
behaviour the fix keeps: names whose numbers match still resolve fuzzily, the oldest of several exact names wins, and an
exact name in another bucket does not answer. Of the 11 that query through the MCP tool, 9 fail on the entity they
answer with; 2, a partial name and an ordinary one, get the same facts as before and fail only on the new output (the
header that names the chosen entity, the per-fact entityId). The other 13 call resolveEntityQuery first. Full suite:
3,717 pass / 0 fail across 190 files (3 todo) on Bun 1.3.14 and on Bun 1.4.2; tsc unchanged. The cross-model adversarial
pass (codex) cleared the fix at its fifth turn, after 7 findings, 4 at the first turn and 1 at each of the next three:
the name search checked numbers only after cutting its candidate list; names that differ only in non-ASCII case missed
the exact step (fixed over three rounds: JavaScript case folding, one FTS5 phrase, then a JavaScript comparison for
names FTS5 tokenizes differently); a header overstated how the name search ranks; a stand-in by slug for an unknown ID
was removed; and padded names lost the exact step. Three more turns cleared these notes and the docs, after 4 findings
(3, then 1): the docs said that reindex --enrich re-extracts every document (corrected wherever it was said), an
example merge that the candidate lookup never proposes, and an inexact summary of the exact-name lookup.
What didn't change
The buckets, the 0.75 and 0.65 thresholds, entity IDs (vault:type:slug), the schema, and kg_query's parameters, date
and direction filters and evidence summaries. Fuzzy matching is otherwise unchanged: two names whose numbers agree still
merge when the candidate lookup finds the existing one through a shared word and their ratio clears the bar (Atlas API
and Atlis API score 0.889). Mentions and triples already stored keep the entity they were given (see Upgrading).
v0.43.1 — input queued between a built-in command and its output no longer makes the command a request
A built-in command (/model sonnet) is a setting change, recognised by the output that follows its record, and the
readers looked for that output in the record's very next row. v0.43.0 began classifying input received while the
assistant was working (a queued_command attachment) as a notice that opens no turn, and the PreCompact reader, which
used to skip such rows, began reading them. When such input was written between a built-in command's record and its
output, both readers took the command for a typed request: PreCompact could save /model sonnet as the last request, a
handoff digest could take it as a turn's opening text, and the observer counted an extra turn. The Stop hooks' reader
had read such a row as the record's successor before v0.43.0 too. In the transcripts surveyed for this fix every
built-in command's output was the row right after its record (4,076 records), so the shape is rare.
What changed
The local-command rule reads past queued input. A command record's successor is the next row that is not a notice opening no turn: in the PreCompact reader, and in the Stop hooks' reader within a read, at a bounded read's end and in the backward scan that finds the current turn. A notice that opens a turn still ends the look, so a record it follows stays a turn.
A record whose successor is not written yet is held back — the read's
nextstops before it and the queued input after it — until a successor arrives, unless the caller knows the turn is over or the record is a prompt command, as a record at the end of a transcript already was. This also holds where a bounded read, or a step of the backward scan that finds the current turn, ends at such a record: through v0.43.0 a record whose next line was still being written could be read there as a request.The transcript classifier's revision is 3, so the observer's checkpoints and the digests of turns still in progress that v0.43.0 wrote are derived again once (see Upgrading).
Tests:
tests/unit/turn-starters.test.ts.Docs:
docs/concepts/architecture.md(Stop pipeline: turns),docs/guides/upgrading.md.
Upgrading
As for v0.43.0: no action needed beyond upgrading every process that shares the vault; restart clawmem watch. When a
range is next processed, an observer checkpoint written by an older version, v0.43.0 included, restarts it from its
first window (the range is not lost), and the digest of a turn still in progress is derived again; settled work is not
redone. See upgrading.
Verification
Twelve new tests were written before the fix and five while it was revised; all 17 were run on v0.43.0: 11 fail, each
for the reason its name gives, and 6 pass. The 6 guard behaviour the fix must keep: a notice that opens a turn ends the
look for a built-in's output, a prompt command's expansion after queued input keeps the command a turn, and a record
released by its caller or a prompt command is not held back. Three of them cover cases that earlier drafts of this fix
got wrong, and each fails on its draft: a record that the backward scan holds back with no turn opener before it anchors
the current turn at itself (two tests), and a complete next line larger than a read is still read whole by the backward
scan. Sixteen mutants, each undoing one part of the fix or of the v0.43.0 behaviour it rewrote, each fail at least one
test: the rule reading past nothing, or past a notice that opens a turn; either reader taking only the very next row;
the trailing record taken as the read's last line only; a look that takes the first line whatever it is, or that stops
at the first queued input; a bounded read that never holds a record back, or that keeps the input after a held-back
record; a backward scan that does not look past a step's end, whose inner steps always release, that keeps its first
step's anchor after a later step holds a record back, or that judges a complete next line from its first 64 KiB; the
classifier's revision left at 2; a look that never demotes; a prompt command held back at a read's end. A seventeenth
mutant only raises memory — the look reading a whole read's lines at a time instead of one — and passes every test, as
expected; that bound is measured instead: on a transcript ending in 96 queued inputs of 1 MiB each, finding the current
turn held at most 15 MiB of lines at a time (v0.43.0: 16 MiB; the first draft of this fix: 96 MiB). Full suite on Bun
1.4.2: 3,683 pass / 0 fail across 189 files (3 todo); tsc unchanged. An earlier full run of this fix failed one timing
test of the eval vector daemon's client deadline (tests/hooks/eval-vector-daemon.integration.test.ts), which this
release does not touch; it passed 5 runs of 5 alone and in every later full run. The adversarial review cleared the fix
at its third turn, after 3 findings, all in the backward scan that finds the current turn: two fixed and pinned by
tests, one (its memory bound) fixed and measured as above. A fourth turn cleared these notes.
What didn't change
A record followed directly by its output, a prompt command and its expansion, what opens a turn, the labels, the read bound and the check of a next line larger than the read. Other rows without a message (an attachment of another kind, a bookkeeping row) between a record and its output are still read as its successor by the Stop hooks' reader, while the PreCompact reader skips them; the surveyed transcripts hold none there.
v0.43.0 — a turn started by a task's notice, another session or a bare command is its own turn
The Stop hooks and the PreCompact hook start a turn at each typed prompt. Through v0.42.0 nothing else started one: a
prompt command run without arguments (/review), a background task's <task-notification> and a message from another
Claude Code session were metadata, so the assistant's reply to any of them was recorded as part of the previous typed
prompt's turn. The observer read that reply as an answer to the earlier prompt, the handoff merged the two turns into
one digest, feedback-loop credited a note the reply named to the earlier prompt's surfacing, and the reason the turn
happened was dropped. Input received while the assistant was working (a queued_command attachment) was read by
nothing, the user's own mid-turn prompts included.
What changed
What opens a turn. A typed prompt or a prompt command, now with or without arguments (
/name); a built-in command is still recognised by the output that follows it, now also in thesystem/local_commandrow newer Claude Code versions write. A background task's notice and another session's message open a turn too: on Claude Code 2.1.278 and later by the row'sturnOrigin, which marks exactly the rows that open a turn (a notice row without it opens none); on older versions and other hosts by the row's shape. Input received while the assistant was working never opens a turn.A notice is a label, never a message body. The observer, the handoff and PreCompact see a notice as
[background task <status>] <summary>(at most 300 characters, never the task's output),[message from <session name>](never the message's text or its sender's socket path),[message from a background agent], or[turn started by <origin>]. A prompt typed while the assistant worked keeps the user's words ([typed while the assistant was working] …, at most 2,000 characters); queued input of unknown origin is labelled without its text. The assistant's reply to a notice is recorded as before, and it may restate the notice.Each consumer follows. The observer numbers turns at every opening and restores a long turn's opening line — the typed request or the label — when its 100-message window drops it. A handoff digest's request is the turn's opening text, and the streamed path for a turn larger than one read now digests exactly what the normal path does, short replies included.
feedback-loopcloses every turn at the next opening, and pairs a surfacing row with a notice's turn when the row's prompt hash equals the hash of the text the hook received for that notice (for another session's message, its<cross-session-message>element); a row with no such match is concluded unattributable, never credited to a neighbouring turn. PreCompact's last request is the last text of ten or more characters the user typed (through v0.42.0, more than ten) — a mid-turn prompt included, a bare command (/pre-compact) never — and a decision's stored context after a notice is the notice's label, not the earlier prompt.A bounded read that ends right after a built-in command with arguments (
/model sonnet) now looks at the next line before classifying it, whatever the caller asked about the read's end; through v0.42.0 such a record could read as a turn start. A next line larger than the read is judged from its first 64 KiB, which now also recognises asystem/local_commandrow by its owntypeandsubtype, whatever its content.Migration, automatic. The observer's checkpoint contract now carries the transcript classifier's revision, so a checkpoint written by v0.42.0 or earlier restarts its range when it is next processed. A handoff digest records how it was derived (
derivRev); a provisional digest (a turn still in progress) written by an older version is derived again when it is re-planned, while settled digests are left as they are.Known limits. A hand-back from a background agent is paired by its row's raw text, because the form the surfacing hook receives for it is unmeasured; it normally does not match, so its turn is normally concluded unattributable. The
context-surfacinghook is unchanged: it still stores the prompt it receives, a task's notice or another session's message included, as its prior-query input. A next line larger than the read is still judged from its first 64 KiB without being parsed: escaped characters, a quote inside a key, a nested field, a host's marker row shaped like command output, or invalid JSON, inside that prefix or past it, can make it misjudge whether the command before it was a built-in, as in v0.42.0. The check of the row's owntypeandsubtypethat this version adds can be misled by the last of these, and it takes asystem/local_commandrow that carries an assistant message for command output, where the full parse reads the assistant's reply.Tests:
tests/unit/turn-starters.test.ts.Docs:
docs/concepts/architecture.md(Stop pipeline: turns; the surfacing hook's stored prompts),docs/guides/upgrading.md,docs/guides/setup-hooks.md,docs/troubleshooting.md,AGENTS.md.
Upgrading
No action needed beyond upgrading every process that shares the vault: the hooks take the new version at their next
run; restart clawmem watch, whose worker runs the same readers. Neither plugin's own files changed, so a copied
OpenClaw or Hermes plugin need not be copied again. When a range is next processed, an observer checkpoint written by
an older version restarts it from its first window (the range is not lost), and the digest of a turn still in progress
is derived again; settled work is not redone. See upgrading.
Verification
Run on v0.42.0, 61 of the 95 new tests fail and 31 pass; the 31 guard behaviour the change must keep (metadata stays
metadata, the hard exclusions, what is never a last request, the trailing command a bounded read holds back, local
output that is not over-matched), and four of those are cases a first draft of this change broke: an assistant message
or a tool result inside another envelope, and metadata that is present but invalid. The other 3 are todo: the limits
above of the check of a line larger than the read. One existing test changed: the PreCompact reader's rendering
contract (tests/unit/compaction-transcript.test.ts) now expects a task's notice as its label; every other row
renders as before. Thirty-four mutants, each undoing one part of the change, each fail at least one new test: an
opening notice that opens nothing; no fallback by shape for older writers; a task's label read from anywhere in the
element or past its header; a peer's body as its label, or its raw row as its identity; an envelope that outranks an
assistant message; invalid metadata read as absent; cuts that split a surrogate pair; a mid-turn prompt that opens a
turn; a bare prompt command as metadata; no recognition of a system/local_command row, in a next line or in the 64
KiB prefix; no look past a read's end; a prompt command held back at the end of a read; a first cursor that skips an
answered notice; a long turn's opening line not restored; turn numbers, digest requests and streamed reads that take
only typed prompts as openings; a digest never derived again, or given to a turn with nothing in it; PreCompact's
ten-character rule, bare and queued commands as a last request, and a mid-turn prompt never one; a notice's label in
the open questions; a checkpoint contract without the classifier's revision; a notice's turn never paired, or never
closing the turn before it. Full suite on Bun 1.4.2: 3,666 pass / 0 fail across 189 files (3 todo); tsc unchanged. The
adversarial review (one session) cleared the design at its fourth turn, after 15 findings, one of which (the surfacing
hook storing what it receives) is outside this change, as Known limits says. It cleared the implementation at its
fifth, after 13: 9 fixed, 2 in code this change dropped, and 2 accepted as the limits above of the check of a line
larger than the read. Six more turns cleared the docs and these notes, after 4 findings.
What didn't change
A typed prompt is read as before, and a built-in command whose output follows it is still a setting change. The read
bound (64 MB; 4 MB for the turns read as context) and v0.42.0's check of a next line larger than the read are kept.
The observer's instructions, parser and grammar, the vault's schema, the context-surfacing hook and clawmem mine
are unchanged.
v0.42.0 — clawmem serve turns web pages away and requires a token
clawmem serve binds 127.0.0.1, and through v0.41.5 that was its only defence, but a web page in your browser can
reach a loopback port. With no token configured — the default — any page could change the vault. A text/plain POST,
which a browser sends without a preflight, was parsed as JSON, so a page could archive documents (/lifecycle/sweep),
restore every archived document (/lifecycle/restore), reindex, rebuild the graphs, or pin, snooze and forget a
document whose docid it knew. A JSON POST got through as well: the preflight answered every origin with *. And with
no Host check, DNS rebinding — a page whose host name is pointed at 127.0.0.1 after it loads — could read the whole
vault from /export.
What changed
A token, on by default. Every request,
/healthincluded, needsAuthorization: Bearer <token>, or it gets401. The token isCLAWMEM_API_TOKENwhen that is set and non-empty. Otherwiseservegenerates one on its first start in the token fileserve-tokenunderCLAWMEM_CONFIG_DIR(default~/.config/clawmem): 43 random characters, mode 0600, published without ever overwriting a file, so two servers starting at once agree. Every token must be 32–4096 characters ofA–Z a–z 0–9 - . _ ~ + /with optional trailing=.serverefuses to start on a shorter or malformed token, on a token file other users can read or that is a symlink, and on a config directory that is not yours or that its group or others can write; its messages name the fix and never contain the token. An emptyCLAWMEM_API_TOKENnow means "use the file", never "open". The comparison takes constant time. The token stops web pages; it is not a permission system against programs running as your user, which can read it.clawmem serve-tokenprints the tokenservewould use under the current environment, creating the file if needed:curl -H "Authorization: Bearer $(clawmem serve-token)" ….clawmem serve --no-tokenserves without a token, on a loopback bind only, with a warning at every start. It is refused on any other bind. Pages from other origins are still refused, but a page served from a loopback origin, or from an entry ofCLAWMEM_ALLOWED_ORIGINS, can call it, and so can any local program.Origin and Host checks run before routing and answer
403. A request is refused when itsOriginnames anything but a loopback host or an entry of the newCLAWMEM_ALLOWED_ORIGINS(Origin: nullincluded), or when itsHostis not a loopback host, a named bind's own address, or an entry of the newCLAWMEM_ALLOWED_HOSTS. On a wildcard bind with noCLAWMEM_ALLOWED_HOSTSthe legitimate Host cannot be known, so it is not checked; the startup log says so, and the token alone stops a rebound page there. Both headers are parsed strictly: credentials, paths, a duplicatedHost, ports outside 1–65535 and invalid addresses are refused, and loopback is recognised in its IPv4 and IPv6 spellings (127.0.0.0/8,[::1],[::ffff:127.0.0.1]).serverefuses to start on an allowlist entry it cannot parse or an empty one, so a stray comma cannot turn the Host check off.JSON bodies only. Every POST, bodyless included, needs
Content-Type: application/json, or it gets415, so a POST from a page on another origin always needs a CORS preflight, which a foreign origin fails.Exact CORS. A preflight from an allowed origin is answered with that origin, never
*, and responses to it carry the sameAccess-Control-Allow-OriginwithVary: Origin; other origins get no CORS headers. Thehttp://localhost:*header, which no browser matched, is gone.A request with no
Hostheader (HTTP/1.0) no longer fails with500: its relative request URL is resolved before routing.The Hermes and OpenClaw plugins learn the token. The Hermes plugin in external mode (its default) sends
CLAWMEM_API_TOKENwhen set and otherwise reads the token file on every call. A plugin that starts its ownserve— the Hermes plugin in managed mode, and the OpenClaw plugin, whose gateway always starts one — first runsclawmem serve-tokenthrough the same binary and environment and passes the printed token to it, so a token in the ClawMem checkout's.env(whichbin/clawmemapplies only to unset variables) can no longer leave a plugin and itsservedisagreeing. It keeps that token when itsserveloses the port to another one started the same way (a systemd service included) and resolves it again only when its agent or gateway restarts. Neither plugin sends an invalid token or puts any part of a token in an error or a log line.Tests: a standing security suite in
tests/security/, and the plugins' token handling intests/unit/rest-clients-token.test.ts.Docs:
docs/reference/rest-api.md(authentication, browser protection, CORS, cross-machine access),docs/reference/configuration.md,docs/reference/cli.md,docs/guides/hermes-plugin.md,docs/guides/openclaw-plugin.md,docs/guides/systemd-services.md,docs/introduction.md,docs/guides/upgrading.md, AGENTS.md.
Upgrading
Every REST client must now send the token. Restart clawmem serve: it generates the token file on its first start,
or uses CLAWMEM_API_TOKEN. Copy the Hermes plugin's contents over the installed one, re-run clawmem setup openclaw
for a copied OpenClaw plugin (a linked one picks it up), and restart the agent. Scripts add
-H "Authorization: Bearer $(clawmem serve-token)" to every request and -H "Content-Type: application/json" to every
POST. A CLAWMEM_API_TOKEN shorter than 32 characters is refused: unset it, or set a random value
(openssl rand -base64 32) for serve and its clients alike. Behind a proxy, or with clients that reach a wildcard
bind by host name, list those names in CLAWMEM_ALLOWED_HOSTS; a browser frontend on a non-loopback origin goes in
CLAWMEM_ALLOWED_ORIGINS. The vault is not touched.
Verification
The new tests were written first and run on v0.41.5. Of the 22 transport tests, 20 failed, each for the reason its
name gives; the 2 that passed are controls (a JSON media type with parameters, and loopback Origins in every
spelling). One of the failures was a defect of its own: a request with no Host header crashed the handler with
500. The 19 guard and token-resolver tests could not run there, since the module they test does not exist; 7 more
were added during review. Of the 9 client tests, the 3 controls passed (the plugins' external calls and OpenClaw's
environment overlay, against a server that was open) and the 6 others failed: the managed Hermes cases, the managed
OpenClaw token, and a header-unsafe token that both plugins sent and logged; 4 more, added during review, fail on
client readers that skip the config-directory check or stop after one short read. Seventeen mutants each fail at least
one test: no Origin check, no Host check, no JSON check, a port read through the URL parser (which drops :80), a
token file opened through a symlink, no 32-character floor, the Hermes plugin ignoring its managed token, OpenClaw's
environment overlay reversed, the Hermes plugin sending an invalid token, empty allowlist entries dropped, a
token-file write that ignores a short byte count, a link-race loser returning its own token, a loser reading the
winner's file unchecked, no startup warnings, no Windows warning, and either plugin's token-file reader stopping after
one read. The six existing test files that start the server now send the token and keep their route-level assertions.
Full suite on Bun 1.4.2: 3,574 pass / 0 fail across 188 files. The adversarial review (one session) cleared the design
at its fourth turn, after 12 findings, and this implementation at its fourth, after 9: 1 noted by a turn that the
reviewer's server ended before its verdict, then 7 and 1.
What didn't change
The routes, their parameters and their responses; the MCP server (stdio, not HTTP); the hooks; serve's default
address and port.
v0.41.5 — a window whose first message fits under a smaller CONTEXT is never held
v0.41.2 fits each observer window in tokens: it takes the fullest form of the window's CONTEXT that leaves 512 tokens
for the transcript, then fits whole turns under it. When the window's first message did not fit under that form, the
window was held as capacity: without trying a smaller form, under which the message fit. A later window's CONTEXT
grows with the transcript's tail, the turn's opening request and the titles recorded so far, so a window
could be held while it fit without them, again on every retry. A format retry's feedback, added to the fullest form,
held a window the same way (capacity: the format retry's feedback leaves no room). The hold's reason also reported
the room under the fullest form, not the real limit. Measured on v0.41.4 with the suite's tokenizer fake at a
4,096-token context, a 1,006-token final answer was held with 551 tokens of room, while the form with only the turn's
request and the titles left 1,222.
What changed
src/observer.tsfitWindowchooses the CONTEXT form as before. When the window's first message does not fit under it, each smaller form is tried in order, fullest first, and the first that fits sends the window. For a later window the forms are the tail with the turn's request (when the tail does not already show it) and the titles, then the request and the titles, then the titles, then none; for window 1, the CONTEXT section, its titles alone, none. A form that repeats an earlier one is tried once. A window that fits under the chosen form is unchanged, byte for byte, with the same token counts in the same order. The line fitting moved, unchanged, intofitLines.A message that fits under no form is still held as
capacity: one message needs N tokens; a window holds W, and W is now the room with no CONTEXT, the real limit. The reason still startscapacity:, soclawmem doctorcounts it as before.A format retry re-fits the same way, so its feedback no longer holds a window that fits under a smaller form.
The Hermes plugin's timestamps follow the file (
src/hermes/__init__.py). A line is stamped as it joins the transcript's writes, under the lock that orders them, so a thread can no longer land a line stamped before another thread's line after it. A new transcript's header is dated by the line it opens, never later: it was stamped at the write, after the first turn's lines, and read 1 ms late about 1 run in 20 of the suite's order check (T29 intests/unit/stop-hermes.test.ts), or later still when the first write waited out a failed attempt's pause. The times still come from the wall clock, so a clock set back can step one back.Docs:
docs/concepts/architecture.md(the CONTEXT forms and the fallback),docs/guides/upgrading.md(v0.41.5),docs/guides/hermes-plugin.md(the transcript format).
Upgrading
No action needed. Ranges held this way replay at their next attempt; clawmem repair stop-queue --retry-now held --run
retries them now. A range still held with capacity: one message needs … after the upgrade has a message larger than a
window with no CONTEXT; raise the server's -c. Hermes: to take the timestamp fix, copy the plugin's contents over the
installed one and restart Hermes (a symlinked install picks it up on its own); a plugin left as it was keeps working. A
checkpointed range resumes where it stopped, and the observer contract is unchanged: a checkpoint records the line
reached, the observations, the titles and a window bound, and none of them is written or read differently. A window sent
under a smaller form is saved like any other.
Verification
Five new tests were written first and run on v0.41.4: the four that exercise the fallback failed, each for the reason
its name gives, and the guard passed. They cover a later window sent under the request-and-titles form, one whose
request already sits in the tail sent under the titles alone, a message that fits no form still held and reporting the
room with no CONTEXT, a format retry sent under a smaller form, and later windows that fit keeping the fullest form.
Each checks its geometry against the prompts the fake counted, so a later change to the prompt text fails a named check
instead of testing nothing. Three mutants (no fallback, the emptiest form only, a stale fixed-part count) each fail at
least one test. For the Hermes timestamps, two tests written first failed on v0.41.4: a new transcript's header dated
after the line it opens, under a clock that moves 1 ms per reading, and a line from a second thread landing after a line
stamped later. With either half of the fix undone, its own test fails. The suite's T29 order check
(tests/unit/stop-hermes.test.ts), which failed about 1 run in 20 on v0.41.4, passed 40 runs of 40. Full suite on Bun
1.4.2: 3,513 pass / 0 fail across 185 files. The adversarial review cleared the window fitter at its second turn, after
3 findings, all in the notes and the tests, and the Hermes fix at its fourth, after 2 findings, a test's synchronization
and an unqualified promise.
What didn't change
The 512-token minimum, the CONTEXT forms and their order, how a window that fits is sized, the halving of a cut reply
and its one-line capacity: limit (a limit on the reply's size, not the prompt's), the format-retry limit, the parser
and the checkpoint contract. The Hermes transcript's lines keep their keys, their order and their timestamp format.
v0.41.4 — the observer's replies parse, and a reply that is not an answer is never "nothing"
v0.41.2 made the observer's prompt fit the server's context. Its replies still failed on the documented observer model. In the vault that found this, 35 ranges were quarantined with "no parseable response within the budget" between the v0.41.2 upgrade and 2026-10-03, and 14 were still held. A read-only re-run of three held ranges against the documented model (qmd-query-expansion-1.7B) got 2 usable answers from 34 replies:
21 replies typed an observation
tool_use: the transcript renders tool calls as[tool_use name="…"], and the parser dropped every block whose type was not on its list.4 replies copied the prompt's own placeholder: the prompt's structure wrote
...in every field.7 replies came back in the model's query-expansion format. v0.41.3 read a short reply without markup as "nothing to record", so a range answered that way committed as empty and was never observed again.
Every format retry repeated its first reply. Its feedback named a
<content>tag the schema does not have, said nothing about which field failed or what it allows, and quoted 500 characters of the bad reply back.
On a backend whose invocations each afford one call, a window that needed a retry also looped. The retry's state
lived inside the invocation, so the invocation ended partial, the range was queued as a continuation (no failure
backoff), and the next invocation sent the same first call again — every minute, indefinitely.
What changed
Only
<none/>means "nothing". The prompt asks for exactly<none/>when nothing significant happened; the sentinel counts alone or inside one complete code fence. An empty reply, prose, or another format is a format failure (empty-reply,no-blocks), never[]. A reply that holds valid blocks and<none/>yields the blocks. The 300-character "plain nothing" rule is gone.The prompt names the types and has nothing to copy. The type rule lists the nine types and says a type is never a transcript role or tool name. The structure's
...placeholders became{{type}},{{title}},{{fact}},{{entity}},{{why}},{{path}}skeleton tokens, which the placeholder guards drop when a model copies them. (A first draft wrote the type list into the structure's<type>line; without the grammar the model copied that line as the type in 28 of 39 replies.) The prompt states the escapes (<>&) and the field limits in characters.A rejected block says why, in classes.
type-not-allowed(with the value's class:tool-role,placeholder,type-list,other),type-missing,title-missing,title-empty,title-placeholder,facts-empty. A block with no usable fact is now rejected (it was kept with no facts). A held range's reason carries the class (no parseable response: type-not-allowed (tool-role)), never transcript text.Feedback that can repair. A format retry names the failing field, its class and the allowed values, and says to output exactly
<none/>if nothing happened. It never mentions<content>and never quotes the reply. The handoff summary keeps its feedback unchanged, byte for byte.A window ends inside the invocation that started it. Up to two format retries per window, each a fresh sample with that feedback. After a reply that needs a retry — unparseable, cut, a validated oversize, or a refused grammar request — every exit before the retry's own reply fails the attempt (
retryable, with the failure backoff, the reason naming that reply's class) instead of queueing a continuation. A one-call backend now takes one backoff step per failure instead of looping.A size reduction persists. A cut reply's halving and an oversize's correction write a shrink-only window bound into the range's checkpoint before the smaller window is tried, so the next processor starts small. Repeated one-call attempts on a cut window converge. Each validated oversize whose
n_ctxis below what/propsclaims also lowers a ceiling for that/propsvalue (7 days; 4 values kept per server), so the next invocation's first call fits.Grammar-constrained decoding on llama.cpp. A server whose
/propsfingerprint is strong (llama-server) and the in-process node-llama-cpp model get a GBNF grammar with each request: every type, predicate and concept enumerated, the fields bounded,<none/>allowed. A refused grammar request — an HTTP 400 (its cause is not claimed) or, in process, a grammar that does not compile — turns the grammar off for that server for 24 hours, and the observer retries without it at once; a grammarless request must reach that server before the grammar is used again.CLAWMEM_OBSERVER_GRAMMAR=offdisables it. Enforcement is not verified before use: completed replies to grammar requests that fail structurally are counted, andclawmem doctorreports them.Units, decoding and guards. Field values are decoded once (
<>&), then trimmed, then checked; every bound counts code points, so a 41-letter astral subject keeps its triple and a long title is cut without splitting a pair. A triple whose subject or object is a tool-call id or a copied rendering (the whole value: an entity such astoolu_abcdef.tskeeps its triple), or whose subject equals its object, is dropped; repeated facts in a block are kept once; an echoed{{entity}}or{{path}}is dropped. A fact or title that restates one of the prompt's own clauses is kept and counted, never dropped.Consolidation fits its prompts (BACKLOG 68.6). Cluster synthesis and the deductive pass count their prompt against the LLM's context and keep the sources that fit; numbering, index bounds, document mapping, the validation context and the statistics all use exactly those. Fewer than two sources fit: no synthesis that tick. A cut reply is not a synthesis or a deduction.
Stale causal runs (BACKLOG 68.2). The "still in progress after 1 h" warning counts runs started 1–24 hours ago. Older ones are reported once, as information: "N unfinished causal run(s) older than 24 hours; not automatically replayed".
clawmem repair stop-queue --retry-now <id[,id…]|held> [--limit N]makes queued ranges due now (held: rows whose last error is not a continuation; claimed rows are never touched; at most 50 by default) and prints their ids. With--run, a pass whose replays all failed no longer ends the drain, and the command reports which of the selected ranges it attempted and when the next work is due, counted with each worker's own due rules (an expired claim is due now; named vaults' feedback mirrors included).clawmem doctorgroups held ranges by class (a v0.41.2–3 reason readslegacy (unclassified)), shows a server's grammar-off record, and counts structural failures under the grammar.
Upgrading
A range's observer checkpoint written by v0.41.2 or v0.41.3 does not match the new contract: its unit restarts at line 0 when next reached. A finished range's tombstone still counts.
Held ranges retry on their own backoff (up to 12 hours apart). To retry them now:
clawmem repair stop-queue --retry-now held --run.Ranges v0.41.3 committed as empty after a query-expansion reply are not re-observed.
The observer's fixed prompt is 110 tokens longer (794 on the documented model, measured): a window holds about 1,660 transcript tokens at
-c 4096and 5,400 at-c 8192.
Verification
The three ranges the investigation started from (held for days, 5 or 6 attempts each: 100, 16 and 8 lines) were
re-run read-only against the documented model (llama-server, -c 8192) with v0.41.4's code, from line 0 as an upgraded
install will. With the grammar, each finished in one invocation; across 93 calls every reply was structurally valid,
81 held at least one observation and 12 were <none/>. Without the grammar, 15 of 29 replies parsed, and 8 of 9
re-runs finished within three invocations. On the same windows, the observer those ranges failed under parsed 2 of 34
replies, and v0.41.3's prompt 4 of 12. The review then tightened the grammar's field boundaries; re-run on the 8-line
range, the server accepted the revised grammar and the range finished in one call.
Fifty-four new tests were written first and run on v0.41.3: 50 failed, each for the reason its name gives, and the 4 guards passed. The implementation review added 25 more, written first the same way: 23 failed before their fix and 2 guards passed. Every new branch has a mutant that a named test kills on an assertion, run from a clean baseline (81 of 81). Full suite on Bun 1.4.2: 3,506 pass / 0 fail across 184 files. The adversarial review (one session) cleared the design at its sixth turn and this implementation at its third, after 15 and then 5 findings; a docs audit of every tracked doc then took three more turns (3, 1 and 0 findings), ending with zero remaining findings.
What didn't change
The handoff summary's prompt and feedback; the legacy
generate()request body (the judge test pins its bytes).No schema migration;
CHECKPOINT_SCHEMAstays 1. Onlyokandemptycommit a range (62.1 D3).The observer model. Content quality on the documented model stays weak, and it answers
<none/>for about one window in eight that holds real work; BACKLOG 69.4 evaluates a stronger model.
v0.41.3 — zerank-2 runs as a Q8_0 GGUF, and the reranker hint stops calling every GGUF broken
Since v0.11.3 the docs said every zerank-2 GGUF is inert, because llama.cpp's standard converter drops
zerank's score head, and that the SOTA reranker must run as the bf16 seq-cls sidecar (~9 GB of VRAM). A
community conversion, seamon67/Zerank-2-GGUF, carries the head. Its Q8_0 file ranks like the sidecar
in about two thirds of the VRAM, so the docs now recommend it. clawmem doctor and
clawmem rerank-health still blamed "the deprecated zerank-2 GGUF" for a degenerate reranker and
pointed at a CLAUDE.md section that no longer exists. Two guides launched zembed-1 without the flags its
last-token pooling needs, and the docs still called the z models non-commercial after ZeroEntropy
relicensed them.
What changed
The zerank-2 reranker can run as a Q8_0 GGUF.
docs/guides/inference-services.mdgives a download pinned to revisionbc7449fwith its sha256, the launch line (llama-server --reranking -c 2048 -b 2048 -ub 2048 --parallel 1), a scoring check and the measurements below. The SOTA stack drops from ~16 GB to ~13 GB of VRAM. The bf16 sidecar inextras/rerankers/zerank-2-seq/stays as the reference, with its reproducible correctness gate. Most other zerank-2 GGUFs still have no score head, and the docs say so. README, AGENTS.md, SKILL.md, the quickstart, troubleshooting and the cloud-embedding, systemd and upgrading guides point at the GGUF route, and the introduction's diagram no longer names the sidecar.The degenerate-reranker hint that
clawmem doctorandclawmem rerank-healthprint now names a zerank-2 GGUF without its score head, offers the Q8_0 GGUF or the sidecar, and points atdocs/guides/inference-services.md. The probe's calibration failure names the same cause and both routes.zembed-1 launch lines. The cloud-embedding guide and the systemd example unit now pass
--pooling lastand--override-kv tokenizer.ggml.add_eos_token=bool:true, as the inference-services guide already did. The systemd guide says to drop both for a mean-pooling model like EmbeddingGemma.License. ZeroEntropy relicensed zerank-2 and zembed-1 from CC-BY-NC-4.0 to Apache-2.0 on 2026-07-24 and ungated their Hugging Face repos. The stack tables, the SOTA sections, the sidecar recipe and the architecture diagram now say commercial use is allowed, and the sidecar's convert step needs no
HF_TOKEN.
Verification
The Q8_0 GGUF was measured against the bf16 sidecar on 240 query–document pairs from a real vault (24
queries): the same top document for 24 of 24 queries, the same top-three set for 24 of 24, mean Kendall
τ 0.994, and scores within 0.021 (mean 0.005), on the same sigmoid(logit/5) scale. Upstream llama.cpp
b11347 ranks the same pairs exactly as the build that measured them (τ 1.000, scores within 0.013). On
an RTX 3090 it uses ~6 GB of VRAM against the sidecar's ~9 GB, at 42 vs 33 ms per document at
ClawMem's request shape. The same repo's Q4_K_M changed the top document for 2 of 24 queries and is not
recommended. clawmem rerank-health passes against the Q8_0 server (coverage 8/8, max score 0.97, min
margin 0.66).
Full suite on Bun 1.4.2: 3,425 pass / 1 fail across 178 files. The failure is a wall-clock test
(tests/hooks/hook-alignment.integration.test.ts) whose second turn ran past its 1,000 ms budget
under the suite's load; it passes alone, 5 of 5, both here and on v0.41.2. The suite's first run also
hit a flake that v0.41.2 already had: two env-override tests in tests/unit/text-similarity.test.ts
re-imported a module with ?t= + Date.now(), so two imports in the same millisecond shared a cached
module (the file failed in 20 of 30 runs on v0.41.2). Each re-import now gets a unique specifier, and
the file passed 30 of 30 runs. The adversarial review (one session) cleared the docs at its second
turn, and the hint fix and the zembed-1 lines at its third, with zero remaining findings.
What didn't change
Only the hint's text changed in code: reranking, the rerank cache, the health thresholds and the in-process fallback behave as in v0.41.2.
The default stack (qwen3-reranker-0.6B) is unchanged, and the sidecar recipe works as before.
No migration, reindex or re-embed is needed for the release itself. A zembed-1 server launched without the two flags may need a re-embed once they are added (see upgrading).
v0.41.2 — the observer counts its prompt in tokens and keeps room for its reply
v0.41.1 bounded the observer's prompt in characters: at most 8,000 for the CONTEXT section and the transcript together, plus a 2,825-character system prompt outside that bound, with up to 2,000 tokens allowed for the reply. On prose that fits the 4,096-token context the docs prescribed for the observer model: prose runs about 5.7 characters per token on that model's tokenizer. Transcripts are not all prose. Measured on the deployed llama-server (qmd-query-expansion-1.7B), hex runs 1.13 characters per token, tool-like lines 1.63, and Chinese, Japanese or Korean text 1.25.
On 2026-10-01 a real vault held one range of 636 KB after four attempts, each ending "no parseable
response within the budget". A read-only reproduction with v0.41.1's modules built an 11,797-character
prompt: 4,072 tokens of the server's 4,096. The server answered finish_reason: "length" after 16
reply tokens (usage: 4,080 + 16 = 4,096). Three attempts measured 4,072, 4,018 and 4,042 tokens, and
every reply stopped inside its first <observation>. Each retry had the same size, so the range was
retried every 12 hours with no way to succeed. In another run the cut reply was 22 characters with no
< in it, and the observer read it as "nothing to record", so that turn's observations were lost
without a quarantine. At -c 8192 the same prompt ended finish_reason: "stop", with 360 reply tokens
for one observation.
The observer now counts tokens against the context the server reports. Before each model call of the
Stop pipeline, retries included, it reads /props (the per-request context: -c divided by
--parallel), else CLAWMEM_LLM_CONTEXT_TOKENS, else an assumed 4,096. It counts the assembled prompt through
/apply-template and /tokenize: on the deployed server that count equalled the chat completion's own
usage.prompt_tokens for every prompt measured, and the refused request's n_prompt_tokens for two
prompts over the context. It keeps 40% of the context for the reply (768 to 2,000 tokens; 1,638 at
4,096) and asks for as many observations as that room holds (4 at 4,096, 5 at 8,192). A batch that
does not fit one prompt runs as windows inside the same Stop, each window seeing the titles of the
observations the earlier ones found. A reply the server reports as cut is never parsed: its window runs
again at half its size, and a reply that did not finish as an answer is held for a retry, never read as
"nothing to record".
Progress through the windows is kept in a durable checkpoint, so a range that runs out of calls or time
resumes after its last finished window instead of starting again, and that pause is not counted as a
failure. A range that can never fit, because one message is larger than a window can be on that
server, is held with a capacity: reason that clawmem doctor counts, and replays by itself once the
context is raised. The handoff summary fits its prompt the same way. The docs now prescribe -c 8192
for the observer model: the fixed part of the observer's prompt measures 684 tokens, so a window holds
1,356 to 1,774 transcript tokens at 4,096 and 5,090 to 5,508 at 8,192, for about 470 MiB more VRAM.
Separately, on 2026-09-30 the embed timer ran an incremental clawmem embed while the embedding server
was down. The geometry canary was unavailable, all 2,202 fragments failed, no vector was written, and
the run still set the geometry taint, which only a verified full clawmem embed --force clears (99
minutes on that vault). A run that did not clear the index and stored nothing cannot have mixed a
second geometry into it, so it no longer sets the taint; it still exits 1. Clearing the taint now also
needs a passing preflight: a --force --force-geometry run over a failed canary could clear it before,
because its end check compares the run's vectors with themselves.
What changed
src/llm.ts:generateDetailed()reports why a reply stopped (stop,length, orotherfor a reason such ascontent_filter) and the server's token usage. A server that reports no reason gets a best-effort reading, logged once per process: a reply that used its whole allowance counts as cut, any other as complete. A 200 without a completion choice, or one that is not JSON, is an error, never an empty answer, and counts toward the endpoint's failure streak like an HTTP error (issue #24's 60-second cooldown) instead of clearing it. It stays on the backend it was given: a remote transport failure returnsunavailableinstead of falling through to in-process generation inside the call. An HTTP 400exceed_context_size_erroris classified ascontext_exceeded; the first one per endpoint does not count toward the 60-second cooldown streak for 10 minutes, and a repeat that is not smaller does.llmCapacity()reads/propsfresh on every call (a strong fingerprint: sha256 of the model path, chat template and build, when/propsnames all three; one that gives the context alone is measured but weak), else the configured or assumed context (a weak one).countChatTokens()counts template-exact through/apply-template+/tokenize(no margin: the count is exact), else the content's count plus the template's overhead, measured by a one-token probe and kept 24 hours, and a margin of 8 (32 when no probe answers), else an estimate that counts hex runs, digits, punctuation and non-ASCII at one character per token and other text at three, scaled by a factor that only rises (margin 32).generate()logs once per process when the server cut a reply at its context; its request body is unchanged, byte for byte. A validated oversize invalidates the measured overhead that fed its count. An LLM injected throughsetDefaultLlamaCppthat implements onlygenerate()is counted by estimate againstCLAWMEM_LLM_CONTEXT_TOKENS, else an assumed 32,768 tokens: most units go in one prompt, the largest the observer takes (100 lines of dense tool output) in two windows, and a smaller configured context windows more; its replies are read as complete.extractObservationsResult()runs one invocation without a checkpoint, so a unit that needs more calls than its budget allows returnsretryableand keeps no progress.src/observer.ts:extractObservationsWindowed()— the reply reserve, the observation count, whole-turn windows (a turn is cut between messages only when it alone exceeds a window), a CONTEXT for window k > 1 made of the transcript just before it (EARLIER IN THIS EXCHANGE) and the titles recorded so far (ALREADY RECORDED), halving on a cut reply, one format retry inside the budget, one re-size from an oversize refusal's own count andn_ctx,capacity:results, and at most 6 model calls per run. The capacity is read before every call and checked against the server the run is pinned to: two strong fingerprints that differ restart the range; a/propsthat stops giving a fingerprint (no answer, an error, a 404, a body that is not llama.cpp's, one without the model, template and build) leaves it waiting with its progress kept, however long. A line that fits a window alone is never held ascapacity:: when the fit's guesses fail, it searches down to one line, and the window still ends at the last turn boundary that fits, the one after its first line included. The measured call time is the mean of the latest 50 model calls.extractSummaryFitted()fits the summary prompt with a 500-token reply, dropping the recent turn text first and then digests from the end, and reports how many digests it used; a digest lists at most 10 files, and the stored summary is capped (the request at 600 characters, each other field at 800).src/stop-checkpoint.ts(new): checkpoints asvault_flagsrowsobserver-ckpt:<session>|<transcript key>|<hook>|<range key>, holding the windows done, the observer's contract version, the server fingerprint and the pinned backend. Create isINSERT OR IGNORE, every advance or reset a compare-and-swap on the row's raw value, and only the range's committing Phase B writes the tombstone. The sweep removes orphans only, paging from where it last stopped so the checkpoints that must stay never hide a newer orphan; a queued range holds a checkpoint only when its full identity matches (the transcript's epoch, the offsets and the bytes).src/stop-extract.ts: each unit loads or matches its checkpoint and saves progress after every window. A unit cut short becomes acontinuation: quarantined with attempts unchanged and due again in 60 s (or at once after a budget skip). A checkpoint whose server cannot be verified waits as a continuation, however long, as for an unreachable backend; it restarts from its first window only on a verified change (another strong fingerprint), and the operator can drop the range instead. The latest 50 measured observer calls, and their mean, are kept in theobserver_call_meanflag. A first-window fallback from the remote to the local backend keeps the run's call cap. A Stop ends its loop at its first quarantine, so one Stop queues at most one range. A replay that finds its range committed elsewhere ends as done without a second set of effects. Observations with equal bodies from different windows merge, with their triples, before the judge. Messages beyond the observer's 100-message view of one turn are counted in theobserver_accumulator_droppedflag.src/stop-worker.ts: a due continuation runs first in each tick, in its own slice (twice the process's mean observer call plus 5 s, 15 to 23 s), a continuation whose claimant died (its lease expired) included, and the tick ends with the checkpoint sweep.src/stop-handoff.ts: the summary step usesextractSummaryFitted(); its watermark moves only past the digests the summary used.src/hooks.ts,src/hooks/decision-extractor.ts: observer messages carry their turn;formatObservationis exported for the grouping.src/clawmem.ts:clawmem embed— a run without--forcethat stored no vector sets no taint (it says so and exits 1), and clearing the taint needs a passing preflight.clawmem doctor— anLLM contextline (the context and its source, how prompts are counted, the window it leaves, and the observer's mean call as the Stop pipeline last measured it, flagged above 18 s) and Stop-pipeline lines forcapacity:holds, queued continuations (with those waiting for a server that could not be verified), live checkpoints no queued range owns — split into those a later Stop can still reach, a first Stop's with no cursor (resumable by a later Stop that reads the same range, until the sweep's 7 days), and those behind the transcript's cursor (a dismissed or superseded range) — and unseen messages.Docs:
docs/guides/inference-services.md,docs/quickstart.mdandCONTRIBUTING.md(-c 8192, the measured window sizes and VRAM cost, servers without/props),docs/reference/configuration.mdandREADME.md(CLAWMEM_LLM_CONTEXT_TOKENS),docs/reference/cli.md(the doctor's new lines, the embed taint rules, the worker's continuation step),docs/concepts/architecture.md(the Stop pipeline's windows, checkpoints, continuations and summary fit),docs/guides/setup-hooks.md,docs/guides/cloud-embedding.md,docs/troubleshooting.md(dense turns, the doctor lines, the context-cut warning, the embed run that wrote nothing),docs/guides/upgrading.md,AGENTS.md,SKILL.md.
Verification
New tests: tests/unit/observer-budget-v0412.test.ts (26: the reply reserve and observation count, dense
hex in windows, cut replies and replies that did not finish, window CONTEXTs, resume and progress, the
compare-and-swap and fingerprint outcomes, an unverified server, the oversize re-size and the overhead it
invalidates, a capacity read before every call, a large line among many small ones, the turn boundary
after a one-line turn, a snapped window counted before it is sent, per-call timing, the contract's
inputs, capacity:, the summary fit, an injected generate()-only LLM up to the largest unit),
tests/unit/llm-budget-v0412.test.ts (19, against a fake llama-server: /props answered, or not — a
404, a 200 that is not llama.cpp's, a 500, one without the model, template or build — and the unverified
pin it leaves, /apply-template, /tokenize, the 400, finish_reason and usage, a reply with no finish
reason (end to end through the observer), a 200 without a choice or not JSON and the cooldown it trips,
the oversize exemption, the overhead's expiry and invalidation, backend pinning),
tests/unit/stop-checkpoint-v0412.test.ts (20: continuations through replays, contract resets, separate
sessions, a range committed elsewhere, an unreachable backend, an unverified server however old its
checkpoint, a /props answering HTML, 404 or the context alone and then the original one (the real
LlamaCpp: the unit waits, then resumes at its saved line), a repeated first Stop with no cursor resuming
its saved window, one held range per Stop, continuations first in the
worker and an expired claim, merged bodies, the call cap across a backend fallback, the kept call time
and its 50-call mean, the checkpoint transitions, the orphan sweep, its paging and its range identity),
tests/unit/doctor-observer-v0412.test.ts (1, the real CLI: queued continuations and the unverified
ones, live checkpoints no queued range owns matched by full identity and split three ways (ahead of
the cursor, a first Stop's with none, behind it), a range dismissed through clawmem repair stop-queue --dismiss, the mean call) and
tests/unit/embed-taint-v0412.test.ts (5, the real CLI against a fake /v1/embeddings that passes the
real canary). In tests/unit/stop-observer.test.ts three token tests replace v0.41.1's two
character-bound tests; tests/unit/canary-validation.test.ts's no-work test now expects no taint.
Against v0.41.1's source, with the two new constants pinned to their specified values: the three token
tests fail (one call where windows were needed; a 100-message unit answered as "nothing to record"
instead of reported; a retry prompt of 3,065 tokens that left 1,031 for the reply), the no-work test
fails, and two of the five embed tests fail (an empty run set the taint; an overridden failed canary
cleared it); the other three pin rules that did not change. The other new files test the new API and do
not load there. The adversarial review's first implementation pass raised fifteen findings. Eighteen
regression tests were written for them: seventeen failed against the code before their fixes, and the
eighteenth pins behaviour that its fix only documents (an injected generate()-only LLM). Its second
pass raised seven more: a 200 without a completion never counted toward the cooldown; a turn that fits a
window whole could be cut after a one-line turn, and a snapped window was sent without its count
checked; a re-anchored transcript's range could hold an older checkpoint; a reply with no finish reason
read as complete; the "latest 50 calls" mean decayed instead; a checkpoint whose server could not be
verified reset after an hour with no evidence of a change; and the injected-LLM claim was too broad.
Twelve regression tests were written for them: eleven failed against the code before their fixes, and
the twelfth pins the corrected claim (the largest unit in two windows). Its third pass raised two more:
the second-pass rule that read a /props answering 404 or a body that is not llama.cpp's as a changed
server could discard finished windows on a transient proxy answer, so that rule was withdrawn (only a
verified differing fingerprint resets); and the doctor promised a resume to checkpoints no later Stop
reaches. Their three regression tests all failed against the code before the fixes. Its fourth pass
raised two more: a /props that gave the context without the model, template and build still made a
strong fingerprint, so a transient answer of that shape reset finished windows (a strong fingerprint
now needs all three); and a first Stop's checkpoint with no cursor can still be resumed by a later Stop
that reads the same range, so the doctor reports those apart. Of the four tests written or extended for
them, three failed against the code before the fixes; the fourth pins the resume the doctor now
reports. Forty-five mutants of the fix
(among them a cut reply parsed; no reply reserve; an advance without compare-and-swap; a sweep that
ignores a queued range, reads one page, or matches a range without its epoch; a Stop that goes on after
its first quarantine; a continuation counted as a failure; an empty run that taints; an overridden
canary that clears; a reply that did not finish, parsed; a malformed 200 that does not strike; a snap
that skips the first turn boundary; a decaying call mean; an unverified checkpoint reset after an hour;
retries that reuse the window's capacity; a fallback with a fresh call cap) each fail at least one
test. The first mutant pass let two through, and both exposed weak tests (a transcript's first Stop
starts at its current turn, so one test never built more than one unit; one doctor count was
symmetric); both tests were fixed.
On the deployed server (llama.cpp, qmd-query-expansion-1.7B, -c 8192), with synthetic text: the
template-exact count equalled usage.prompt_tokens for five prompts (15, 733, 2,136, 642 and 1,994
tokens: short, prose, hex, Chinese and Japanese with emoji, JSON); the doctor's window is 5,508 tokens
(684 fixed, 2,000 reply). A dense synthetic turn ran in one call at 8,192 (prompt 3,636, reply 233)
and, with the observer's budget capped at 4,096, in 7 windows, every reply ending
finish_reason: "stop": prompts of 1,997 to 2,111 tokens against a limit of 2,458, replies of 177 to
921 (measured again on the final code). Full suite: 3,426 pass / 0 fail across 178 files (Bun 1.4.2).
tsc unchanged. The adversarial review (one session) cleared the design at its tenth turn and this
implementation at its fifteenth, with zero remaining findings.
What didn't change
Batches are packed as in v0.41.1 (100 messages, 8,000 characters less the CONTEXT's and a retry's share); windows split a batch only when it does not fit one prompt. The observer still reads at most the last 100 messages of one turn;
clawmem doctornow counts the ones it skipped.Turns a session's last Stop did not reach (its time ran out, or it held a range) are extracted only by a later Stop of the same transcript, as in v0.41.1; a watcher catch-up is planned.
An embed run killed after it stored vectors (
SIGKILL, out of memory, power loss) sets no taint, as in v0.41.1, because it never reaches its end check. A taint set by v0.41.1 or earlier stays until a verifiedclawmem embed --force.Consolidation's prompts and the exported
extractSummary()still go through plaingenerate().A small observer model may still record a decision again in a later window that shows it again; equal bodies merge, others stay.
No migration: checkpoints live in
vault_flags, continuations instop_retries.
v0.41.1 — the observer's prompt fits its documented context again
v0.41.0 gave the observer a CONTEXT section: the two turns before the batch and the session's recorded
observation titles, marked as already recorded. The section rendered those turns through
prepareTranscript with that function's whole budget, 8,000 characters, and listed up to 30 titles,
while the batch's own transcript kept its 8,000. At its largest the prompt grew from about 10,900
characters to about 23,600: from 2,470 tokens to 5,382 on the observer model the docs prescribe
(qmd-query-expansion-1.7B, counted by its server with the chat template, on prose from this
repository's docs). The docs run that model with -c 4096, and the server refuses a longer prompt
with HTTP 400. The observer retries a failed call twice, so such a batch went to the server three
times and was refused three times. The third refusal in a row put the process's LLM endpoint into
its 60-second cooldown: that attempt, and the process's other LLM calls for the next minute, ran
in-process through node-llama-cpp (downloading the model on first use), or not at all with
CLAWMEM_NO_LOCAL_MODELS=true. A batch left without an answer was quarantined in stop_retries as
model unavailable, and every replay sent the same prompt again. A prompt under the limit could
still leave the model little room: on one real Claude Code turn the prompt was 3,875 tokens, which
left 221 of the 4,096 for an answer the observer allows 2,000 tokens.
The section now has a bound of its own, and the transcript gets what the section leaves of the render budget, so the two together, with a retry's error feedback when there is one, stay within the 8,000 characters v0.40's transcript alone could take. The bound is in characters, not tokens. On the same prose the largest prompt is now 2,403 tokens, and the real turn above is 1,473; text that tokenizes more densely (Chinese, Japanese or Korean, for instance) can still pass 4,096 tokens within it, as it could in v0.40.
Separately, the test suite read the developer's own ClawMem configuration. loadVaultConfig()
reads ~/.config/clawmem/config.yaml unless CLAWMEM_CONFIG_DIR is set, and the test-mode guard in
getDefaultDbPath covers only the general vault. A test that runs feedbackLoop without explicit
vaults opens every configured named vault, so on a machine with a skill vault configured,
bun test tests/unit/stop-feedback.test.ts opened that real vault writable, and the v0.41 migration
installed the stop-pipeline schema and its fence triggers on it (it wrote no stop-pipeline rows).
Every test file runs in one process, and one of them deleted CLAWMEM_CONFIG_DIR after each of its
tests, so even a run that set the variable exposed the real configuration to every test after that
file. Every run now starts from an empty scratch configuration and keeps it, and so do the processes the
tests start. A vault migrated that way keeps the v0.41 schema, as any v0.41 process opening it would
leave it. Before an older ClawMem uses it again, stop every v0.41 or later process that shares it,
then drop the fence with clawmem repair counters --remove-fence: an upgraded process reinstalls the
fence at its next writable open.
What changed
src/observer.ts: the CONTEXT section is at mostOBSERVER_CONTEXT_MAX_CHARS(2,000) characters. The prior turns get 1,100 of them, cut from the front when the turns' first request and final response alone are longer, so the latest text stays. The titles get 700: the newest that fit, in order, each at most 100 characters. Neither cut leaves half of a surrogate pair (an emoji cut in two makes llama-server refuse the request with HTTP 500). The transcript getsOBSERVER_MAX_RENDER_CHARS(8,000) less the section's length, and on a retry less the retry's error feedback too (OBSERVER_RETRY_FEEDBACK_MAX_CHARS, 850: the parse error and up to 500 characters of the answer), through a newretryPromptoption ofwithRetryAndFeedback(src/llm-retry.ts); its other callers keep their retry prompt, byte for byte.prepareTranscripttakes its budget as a parameter and never returns more than it; at its default, 8,000, its output is unchanged.src/stop-extract.ts: turns are packed into batches that leave 2,850 characters free (OBSERVER_BATCH_RESERVED_CHARS: the section's 2,000 and a retry's 850;packTurnBatcheshad thereservedCharsbound, and nothing passed it), so a packed batch reaches the model whole on every attempt. A single turn larger than a batch is cut to its budget, as before, now 8,000 less the section (and on a retry, the feedback) instead of 8,000.tests/preload.ts, loaded before every test file throughbunfig.toml(Bun readsbunfig.tomlonly from the directory it runs in, so runbun testfrom the repository root): pointsCLAWMEM_CONFIG_DIRat an empty scratch directory, and back at it before and after every test that leaves it unset, and clearsCLAWMEM_VAULTSandINDEX_PATH, which a launcher may export with the real paths. A test that needs a configuration, a named vault or an index path sets its own, as before. Two tests that start the CLI with a cleaned environment (tests/hooks/eval-vector-daemon.integration.test.ts,tests/hooks/hook-replay.integration.test.ts) droppedCLAWMEM_CONFIG_DIRwith the rest, so the child read the real configuration; they now give it a scratch one.Docs:
docs/concepts/architecture.mdanddocs/guides/setup-hooks.md(the section's bound),docs/guides/inference-services.md(the observer runs on the LLM server; what-c 4096fits and when to raise it; which HTTP errors trip the cooldown, stale since v0.37.0),docs/reference/configuration.md,README.md,AGENTS.md,docs/quickstart.md,docs/guides/cloud-embedding.mdanddocs/internals/entity-resolution.md(the LLM server also serves the observer;cloud-embedding.mdalso says the LLM can be a cloud endpoint, which then receives session transcripts, and, withdocs/guides/systemd-services.md, that a fallback is logged),README.md's observer section (a batch the model cannot answer is quarantined and replayed, not a regex fallback, since v0.41.0; the prompt's bound),docs/troubleshooting.md(a new Hooks entry),docs/guides/upgrading.md,CONTRIBUTING.md,docs/contributing.md,docs/quickstart.mdand the pull-request template (the isolated suite,tests/preload.ts, run from the repository root),SKILL.md.
Verification
tests/unit/stop-observer.test.ts adds six tests at the observer: the largest CONTEXT beside a batch that
fills the budget (CONTEXT and transcript within 8,000 characters), the section's own bound and its
latest material (the last prior message, the newest titles), a retry after the longest parse error
the observer reports and a long answer (within the bound, its feedback within 850 characters), a
batch at the packing bound (whole on the first attempt and on the retry), the section's cuts through
emoji (no lone surrogate), and prepareTranscript under a small budget.
tests/unit/stop-extract.test.ts adds two at the Stop hook: a 40-turn backlog packed into several
batches (every prompt within the bound, every turn sent whole exactly once), and the same backlog when
every batch's first answer fails to parse (every retry within the bound, every turn whole in a
retry). tests/unit/llm-retry.test.ts adds two: the default retry prompt, byte for byte, and a
caller's retryPrompt. Nine of these ten fail against v0.41.0's source; the tenth pins the default
retry prompt, which did not change. tests/unit/test-isolation.test.ts adds four: the preload's
scratch configuration, a test that deletes CLAWMEM_CONFIG_DIR and the test after it (still on the
scratch directory, seeing only scratch vaults), and a child process started with the suite's
environment (the same scratch configuration). Three of them fail without the preload; the fourth is
the deleting step. Eleven mutants of the fix (no batch reserve, the transcript's full budget, the
prior turns' full budget, the oldest titles, no final cut, no preload, no repair hooks, a front cut or
a title clip that splits a surrogate pair, no retry hook, a reserve without the retry's share) each
fail at least one of these tests.
Measured with the observer server's own tokenizer and chat template (qmd-query-expansion-1.7B,
-c 4096): on prose from the docs the largest prompt is 2,403 tokens (v0.41.0: 5,382; v0.40's
shape: 2,470), and the second Stop's prompt on a real Claude Code transcript 1,473 (v0.41.0: 3,875).
Full suite: 3354 pass / 0 fail on Bun 1.3.14. On Bun 1.4.2, 3353 pass and 1 fail: Issue #13's
concurrent first-open test (SQLITE_BUSY at PRAGMA journal_mode = WAL), which fails now and then
on an unmodified v0.40.3 as well, and passed five reruns. tsc unchanged. Cross-model
adversarial review (codex / GPT-6, one pinned session): turn 1 raised five findings, four Medium (a
retry's feedback could push the prompt past the bound; two test subprocesses read the developer's
configuration; the notes promised a token guarantee the character bound does not give; the
downgrade step lacked its precondition) and one Low (when a quarantined range replays); turn 2 one
Low (the fallback is not necessarily on the CPU). All were fixed. Turns 4 to 7, on the docs audit
that followed, raised eleven more, six Medium and five Low: a token guarantee the docs still implied,
the scope of the fallback and cooldown statements, the LLM's cloud option (which then receives
session transcripts), and stale role lists and test instructions. All were fixed. It cleared at
turn 8 with zero remaining findings.
What didn't change
The render budget and the output budget: 8,000 characters of CONTEXT and transcript, and up to 2,000 tokens of answer. On dense text a prompt at the bound still leaves the model less than 2,000 tokens of a 4,096-token context to answer in, as v0.40's did.
Ranges v0.41.0 quarantined are due again at most 12 hours after their last attempt, and replay when the watcher or a later Stop next runs;
clawmem repair stop-queueshows how many are queued. No migration.
v0.41.0 — the Stop hooks process each turn once, and feedback counts only verified references
Claude Code runs the Stop hooks after every response, and through v0.40.3 each run started over.
decision-extractor re-read the last 200 transcript entries and ran the observer on all of them,
handoff-generator summarised the same window again, and feedback-loop credited every note the
session had surfaced that the assistant mentioned anywhere in that window. A turn was therefore
extracted, summarised and counted once per later response:
access_countgrew with every Stop, not with every reference (up to 13,213 on one document on the host where this was measured), and co-activation counts andusagerelations grew with it. Relation weights were summed without a bound (the largest was 8,948), so one relation recorded again at every Stop outweighed every other edge wherever relations are ranked by weight.Recall events were matched to turns by position, so a skipped or deduplicated prompt moved every later turn's attribution onto the wrong context.
The session's decision document was rewritten from the last window whenever its decisions changed, so a turn's decisions left it once the turn left the window. A new decision document was not written at all when a vector search found any
_clawmemdocument at least 0.92 similar.The antipattern writer replaced the body of the most recent antipattern document of the last 7 days, whichever session wrote it, so one session's list overwrote another's. On the measured host that document had 4,253 revisions, and 1,912 of 1,923 earlier bodies were held by no document.
A failed summary replaced the session's handoff with a regex fallback.
From v0.41.0 decision-extractor and handoff-generator keep a cursor per transcript and process
only what they have not processed yet, and feedback-loop decides each surfaced turn once, so every
turn is extracted, digested and credited once. Pre-upgrade history is not replayed: a transcript's
first Stop starts at its current turn (a Hermes transcript begun after the upgrade, at its first
line).
What changed
decision-extractor reads the complete turns after its cursor and sends them to the observer in batches that fit its bounds. The two turns before a batch and this session's recorded observation titles go with it as context, marked as already recorded. A batch goes to the model only when its new section holds an assistant message of at least 40 characters or a tool call, which replaces the "at least 4 messages" rule. A batch whose model call fails or times out is quarantined instead of committed empty, and so is a Stop's first batch when too little budget is left to start it (later batches wait for the next Stop). The cursor moves past a quarantined range, and later Stops and the watcher retry it after 1 minute, 5 minutes, 30 minutes, 2 hours, then every 12 hours. A quarantined range whose bytes changed in the meantime is marked unavailable and never guessed at.
Per-session documents, rendered from items. Decisions and antipatterns are stored as items (
stop_items), one row per item, deduplicated only when the same item comes again with identical content. Each session's decision, antipattern and handoff documents are rendered from its own items at a path fixed when the document is first written:_clawmem/decisions/<date>-<sid8>.md, and the same underantipatterns/andhandoffs/, where<date>is the first render's date. A second transcript of the same session id gets-<tk6>after that, and so does a session whose path is already held by a document this pipeline did not write (a pre-upgrade document stays as it is). The merge policies (dedup_check,merge_recent,update_existing) are gone. A session document is never merged with, skipped for, or overwritten by another session's, and the 30-minute hash window ofsaveMemoryno longer applies to it. A forgotten or archived session document is not rewritten; its items stay, and a restore renders them again.The contradiction judge remembers the pairs it decided, and a pair whose older document changed after the call is re-judged against the current content (
judge_deferred) rather than applied.The causal step (
CLAWMEM_CAUSAL_WRITER=shadow|on; the default is stilloff) runs at most once per committed range, under a run key derived from that range. A range committed while the writer is on waits in a queue when the writer is later turned off, andclawmem doctorcounts it.handoff-generator records a digest of every new turn at each Stop, without a model call: the request (at most 200 characters), the last paragraph of the final answer (at most 300), and the files its Edit, Write, MultiEdit and NotebookEdit calls touched. The summary step then runs when at least 3 new digests wait, 30 minutes after the last summary, or when the session has none yet. It is incremental: the previous summary, the new digests and the text of the latest turns that fit. A failed summary changes nothing but its audit, and the digests wait for the next attempt. The regex fallback is gone; without a model, the handoff shows the digests. As before, a transcript gets a handoff once it holds four messages.
SessionEnd flush.
clawmem setup hooksnow also installshandoff-generatorunder SessionEnd, timeout 2 s. At session end it renders the summary and the latest 20 digests past it (a line counts any earlier ones) into the handoff document, and records that the session ended. It reads no transcript and calls no model, and stops itself after 1 s. Claude Code allows SessionEnd hooks 1.5 s. The OpenClaw plugin'ssession_endwaits for the same flush (at most 5 s) before it clears the session's state.feedback-loop credits each turn once, by verified reference. When
context-surfacinginjects context, the bookkeeping drainer records the turn's manifest: every document injected, from every vault, with its title as rendered. Each document counts as surfaced once there. A turn is paired with its surfacing row by the prompt's hash and the host's own ordering of events, never by position; zero or several candidates leave the row unattributed. The reference test runs once per turn over the whole manifest: a display path, a path of at least two segments, a file name as a whole token (a generic one such asSKILL.mdonly with its parent directory), or a displayed title of at least 12 characters and two words. Each must name exactly one entry of the manifest. A verified reference then applies once:access_count+ 1,last_accessed_at, the utility signal, the recall event, and co-activation andusagerelations between the documents that turn referenced. Named vaults apply their own slice of that verdict.A verdict is final when the turn is over: a later prompt, a Stop, the session's end, or, on Claude Code, the summary entry Claude Code writes after each Stop (
stop_hook_summarywithout a hook label, orturn_duration). Where no later entry can change a turn's pairing (OpenClaw, Hermes), the watcher credits a quiet transcript's trailing turn provisionally and makes the verdict final at the next of those. Age alone never makes a verdict final.Counters recomputed once from verified references. The first
clawmem watchstart on v0.41.0 (orclawmem repair counters --apply) recomputes, for the general vault and every named vault:access_count= verified references,last_accessed_at= the newest verified reference (elsemodified_at), the utility signals, and the co-activation counts andusagerelations, whose historical rows are deleted and rebuilt from verified same-turn references (weight 1.0). Right after the upgrade almost nothing is verified yet, so these start near zero. Usage rows written before the upgrade are frozen (pre-upgrade) and never credited. Every value the recompute changes or deletes is kept incounter_repair_log, andclawmem repair counters --restore <op>puts it back while the value still equals what the recompute wrote.Archive grace. A document whose old last access fell inside its archive window gets
access_grace_until= recompute time + 30 days + (document id mod 60) days, and the lifecycle sweep does not treat it as unaccessed before that. The grace ends are spread over 60 days, so the recompute does not make every such document archivable at once.clawmem doctorshows how many grace periods end in each coming week.Injection no longer records co-activations, for any hook: through v0.40.3,
session-bootstrapandstaleness-checkstill recorded one for every pair of documents they injected.Relation weights are clamped to [0, 1], both when a relation is written and wherever a weight is read: graph traversal, A-MEM evolution neighbours, deductive guardrails, and the surfacing hook's relation snippets.
The fence. An older ClawMem that still runs against a migrated vault cannot write feedback counters, co-activations,
usagerelations, utility signals, or documents under_clawmem/decisions/,antipatterns/,handoffs/orobservations/. Each such write is skipped and counted inlegacy_writer_log. Its surfacing hook's usage-row insert fails instead, so that hook injects nothing.clawmem doctorfails while the last such write is less than 24 h old. Upgrade every process that shares the vault.The watcher runs the stop pipeline.
clawmem watchruns a worker every 60 s that does the work no later Stop will: feedback of transcripts that have been quiet for 10 minutes or have ended, named-vault slices, handoff digests after a final Stop that never ran, handoff renders, quarantined ranges, deferred judge pairs and queued causal steps. At its first start it preserves the overwritten antipattern bodies, then runs the recompute. It runs even when no collection is configured, whereclawmem watchused to exit with an error. Without the watcher, each Stop attributes its own transcript and retries at most one due range of its session, deferred judge pairs wait, andclawmem repair stop-queue --rundrains every queue by hand.New commands:
clawmem repair counters [--apply] [--restore <op>] [--remove-fence] [--force](a dry run without--apply),clawmem repair stop-queue [--run] [--dismiss <id>] [--dismiss-causal], andclawmem recover antipatterns [--apply] [--min-occurrences N], which lists the distinct- **Avoid:**lines of the overwritten bodies with counts and dates, and with--applywrites them to_clawmem/antipatterns/recovered-<YYYY-MM>.md.clawmem doctorgains a stop-pipeline section: the migration and fence, an older writer caught by the fence, the recompute, queue depths (warning past 24 h), provisional verdicts, causal steps waiting while the writer is off, preserved antipattern bodies and the grace projection. It also warns when the SessionEnd flush is not installed.clawmem statusgains one line.Hermes. The plugin now runs the three Stop hooks after every synced turn, one pass at a time in the background, and at session end runs that transcript's final pass, the SessionEnd flush and a last feedback run. Under v0.41.0's cursors the old plugin, which ran them at session end only, would keep a session's last turn and write no handoff; a Hermes transcript begun after the upgrade is read from its first line, so a late first pass loses nothing. Hermes prefetches context after a turn for a later prompt (or none: trivial prompts skip it, late results are dropped), so
context-surfacingnow hands the plugin its usage row's id, and the plugin records what became of each row: it writes the id on the user line of the turn it handed the context to, or records the row dropped or unresolved. A transcript write that fails is kept and written later, from its next byte; a record is lost only in the cases the Hermes guide lists, and a row whose record is lost is never credited on it (complete lines already on disk are still read). A process holds its transcript locked while it writes it, so a second process on the same session writes a transcript of its own (<session_id>.2.jsonl). The feedback step reads each Hermes transcript once, every pass resuming where the last stopped. The row is credited in that turn only, or closednot-delivered(host: "hermes"). New transcripts open with a small timestamped header, and lines carry millisecond timestamps. Copy the plugin again when you upgrade — its contents, over the installed one (cp -r src/hermes/. <plugin dir>/, see upgrading).OpenClaw passes the host, the session key and the resolved transcript path to the hooks it runs, and registers a short or empty prompt's transcript without running retrieval, so the watcher can reach every transcript OpenClaw resolved.
Schema. A writable open adds, in one transaction: the stop-pipeline tables (
feedback_ledger,feedback_turns,stop_cursors,stop_retries,stop_items,session_docs,session_transcripts,hermes_scan,hermes_marks,causal_due,judge_deferred,judge_pair_verdicts,legacy_writer_log,counter_repair_log,recovered_antipattern_bodies, andutility_signals, which onlyfeedback-loopused to create), the stamp and identity columns, and the fence. If that transaction cannot commit, the store still opens, the Stop hooks skip their counter and cursor work,clawmem doctorfails, and the next writable open tries again.CLAWMEM_STOP_BUDGET_MSnow bounds all three Stop hooks: the extractor's model phases, the handoff's summary step and the feedback read.Docs:
docs/guides/upgrading.md,docs/guides/setup-hooks.md,docs/guides/hermes-plugin.md,docs/guides/openclaw-plugin.md,docs/guides/systemd-services.md,docs/concepts/hooks-vs-mcp.md,docs/concepts/composite-scoring.md,docs/concepts/architecture.md,docs/troubleshooting.md,docs/reference/cli.md,docs/reference/configuration.md,docs/contributing.md,AGENTS.md,SKILL.md,README.md.
Upgrading
Stop every ClawMem process that shares the vault (the watcher, clawmem serve, MCP servers in open
sessions, the OpenClaw and Hermes plugins), upgrade them all, re-run clawmem setup hooks for the
SessionEnd flush, copy the Hermes plugin again if you use it, then start clawmem watch. Its first
start preserves the overwritten antipattern bodies and recomputes the counters; on a large vault
this takes minutes and writes one before-image per changed value (about 380,000 rows on the measured
vault). Details, the restore path and the downgrade path: upgrading.
Verification
Seventeen new test files, tests/unit/stop-*.test.ts: 252 tests. They drive the real hooks —
decision-extractor, handoff-generator, feedback-loop, the bookkeeping drainer, the SessionEnd
flush through the real clawmem hook process, the OpenClaw engine, clawmem watch on a vault with
no collections, and the Hermes plugin under Python with a stub Hermes, a fake clawmem binary and
real partial writes (a file-size limit) — over transcripts in each host's shape, with a fake
observer, summarizer and judge. Each design test asserts a result v0.40.3 gets wrong; measured on
v0.40.3 with the same fixtures: two Stops with no new turn made two observer calls and two
summarizer calls; a turn credited at one Stop was credited again at the next (access, surfaced and
referenced counts 1 → 2); turn 1's decision was gone from the session document after 120 turns; a
failed summary replaced the handoff with the regex fallback; and two transcripts of one session id
shared one handoff. Every fix a review turn asked for has a test that fails with the fix reverted,
apart from one lock-ordering fix in the Hermes plugin, which rests on its ordering argument. The
SessionEnd flush took 162–173 ms through the real hook process on an idle vault and 431 ms with the
vault held by another writer. Full suite: 3340 pass / 0 fail; tsc reports the same 85 errors as
v0.40.3. Cross-model adversarial review (codex / GPT-6, one pinned session): the design took 22
turns and 83 findings before any code, two of them settled by maintainer rulings; the implementation
took five more turns and 22 findings (10 High); the docs, and the Hermes and doctor defects found
while writing them, took 10 more turns and 51 findings. It cleared at turn 37 with zero remaining
findings.
What didn't change
The observer, judge and causal-writer prompts and models;
CLAWMEM_CAUSAL_WRITERstill defaults tooff, and contradiction analysis still needs a configured judge.What
context-surfacingretrieves and injects. Its usage rows gain the prompt hash, transcript key, host and session key.The composite formula. Its access and co-activation inputs are recomputed as above, so rankings that leaned on inflated counts move once.
Paraphrases are not recognised as duplicates: the same decision stated in other words is a second item, as before.
Transcripts are read as append-only. A transcript that is replaced, truncated or rewritten starts a new generation at its current turn; a branch made with
/rewindis read as the file's lines stand.Pre-upgrade turns are not re-extracted, and the stop pipeline reaches an OpenClaw transcript only once some invocation of its session has resolved the file.
Downgrading needs
clawmem repair counters --remove-fencefirst, or the older version's Stop-hook writes are ignored and its surfacing hook injects nothing. Any v0.41 writable open installs the fence again.
v0.40.3 — the watcher watches directories made after it starts
clawmem watch walked each collection path once, when it started, and watched the directories it
found. Nothing added a watch later, so a directory made afterwards went unwatched until the watcher
restarted, and the .md files in it reached the vault only on a full pass (clawmem update). On a
Claude Code host a new project directory, and later its memory/ directory, appears several times
a day, so the auto-memory collection missed the first memories of every new project. A watched
directory deleted and made again at the same path fared no better: its old watch received nothing
for the new directory, and on ext4 the new directory even gets the old inode number back.
The rescan every event schedules (v0.40.2) now also looks at the directory's subdirectories, with
the startup walk's rules: excluded names (gits, node_modules, …) and .-prefixed names are
skipped. A symlink is not followed, as the index pass does not follow one, unless it is a collection
path itself, which the pass scans from. A subdirectory nobody watches is watched from then on. It is
watched before it is listed, so a file made in between reaches the listing or the watch; each .md
file it already holds is re-indexed; and the directories under it are taken on the same way,
breadth-first, so a tree made in one go (mkdir -p, a copy, a move into the collection) is watched
to its depth. Each directory of such a tree is decided on its own, so a collection path nested
inside it is taken on for its own collection. A watched subdirectory that is gone stops being
watched, with the watched directories under it, and each file it held is re-indexed once more, so
the removal reaches the vault. A directory is known by its device, inode and birth time, so one
deleted and made again at the same path is watched anew instead of staying on its dead watch.
What changed
src/watcher.ts: the subdirectory pass in the rescan;adopt(watch, then list, breadth-first);retire(a gone or replaced directory and the watched directories under it); the directory identity; the rescan checks its own directory first, so a watched directory that is gone or was replaced is handled even when its parent is not watched. Each collection path counts the directories it watches, new ones included, againstCLAWMEM_WATCH_MAX_DIRS. A new directory counts for every collection path that watches its parent other than through a symlink, and for one whose own path it is. When none of them has room, the directory is skipped with the tree under it, the log printsWARNING: <path> is at its cap of <cap> watched dirsonce per collection path, and directories made while it stays at the cap go unwatched (a watched directory that is removed frees its place). A collection path whose startup walk was over the cap watches no new directory, so the directories the cap left out at the start never slip in later. Taking on a tree yields to the event loop every 256 entries, as a rescan does, so a large tree copied into a collection does not hold up the watcher's other work (the vector daemon among it). A new directory that cannot be watched yet (its permissions, the kernel's watch limit) is tried again at each rescan of its parent and reported once. A replaced directory is watched again, path by path, by the collection paths that watched it and the directories under it before; one over the cap at the start takes on nothing its walk left out. The log names each directory the watcher takes on ([watcher] new directory <path>: watching N dirs, orreplaced directory).Docs:
docs/troubleshooting.md(a new Indexing entry),docs/reference/configuration.md(the cap counts new directories),docs/guides/upgrading.md,AGENTS.md,SKILL.md.
Verification
tests/unit/watcher.test.ts adds twenty-three tests that drive the real watcher: a directory made
after the start (a file written in it later is delivered), the files a new directory already holds
(once each), a new directory holding more files than one batch (each once), a tree made with
mkdir -p (watched to its depth), a new directory whose event Bun < 1.4.0 folds into a file's,
excluded and hidden directories (never watched), a new symlink to a directory (not followed), a
symlinked directory the startup walk watched (keeps its watch; a directory made inside it is not
taken on, since the index pass never reaches it; replaced by a real directory, it is watched as
one), a collection path made after the start as a symlink (watched through a parent another path
watches), a collection path nested in a new tree (taken on for its own collection, even with the
outer one at its cap, and when it is a symlink), a new directory that cannot be watched yet
(reported once, watched once it can be), a watched directory deleted and made again (the same inode
on ext4), one another directory is renamed over, a replaced directory holding another collection
path (re-watched for it, even when that path was over the cap at the start, but never the
directories its startup walk left out), a watched directory removed with its files (each delivered
once, and its path made again is watched as new), the cap (new directories past it stay unwatched,
with one warning), a collection path over the cap at the start (watches no new directory),
overlapping collection paths (one watch, one delivery) and close(). Seventeen of them fail against
the v0.40.2 watcher under both Bun 1.3.14 and 1.4.2; the other six guard behaviour v0.40.2 already
had.
tests/integration/cmdwatch-new-dirs.integration.test.ts runs the real clawmem watch with a
Claude Code auto-memory collection: a project and its memory/ made in one go, a file written later
in that memory/, and a project made first with its memory/ added later (then an atomic save in
it) all reach the index; against the v0.40.2 watcher the first step never indexes. Full suite: 3103
pass / 0 fail on Bun 1.3.14 and on Bun 1.4.2; tsc unchanged. Cross-model adversarial review (codex /
GPT-6, one pinned session) took seven turns and thirteen findings, ten Medium and three Low, all
fixed. They included a failed watch that was never tried again, an adoption that held the event loop
on a large tree, symlinked directories the index pass never reads (taken on, and able to shadow a
real path), collection paths nested in a new tree, and replacements that re-watched too little, or
too much for a collection path over the cap at the start. It cleared at turn 7 with zero remaining
findings.
What didn't change
What the watcher watches at start, the per-file debounce, the rescan's comparison of files, the pre-check and routing (
watchTargets).Each delivered file still starts an index pass of its collection, so a new directory that holds N files costs N passes, as a burst of N changed files already did.
New directories count against the cap. On a Claude Code host each session that saves tool results adds a directory or two under its project, so a watcher that runs for weeks on the default cap (500) can reach it; raise
CLAWMEM_WATCH_MAX_DIRSwhen the at-cap warning appears.A collection path that does not exist when the watcher starts is watched once it appears only if its parent directory is watched under another collection path.
The startup walk still follows a symlink to a directory and watches it, although the index pass does not look inside one. A symlink made after the start is taken on only when it is a collection path itself, and a directory made inside a symlinked one is not taken on.
On a filesystem without birth times, a directory deleted and made again with the same inode number is not told apart from the old one, and keeps the old, dead watch until the watcher restarts.
Nothing to migrate. Restart the watcher, then run
clawmem updateonce to index what the old watcher missed in directories made while it ran.
v0.40.2 — the watcher re-indexes files saved atomically and files changed together
clawmem watch acts on the file name each change event carries. Bun before 1.4.0 folds the events
that reach one watched directory together into one callback per event type, named after the first
file; Bun 1.4.0 reports each event under its own name. Three common changes lost their name on the
way:
an atomic save — a temp file written beside the target and renamed over it, which is how many editors, atomic-write libraries and agent tools save (Claude Code's Write and Edit tools among them) — arrived under the temp file's name (
notes.md.tmp.4242.9f3c), and the watcher's.mdfilter dropped it;a rename inside one directory (
draft.md→notes.md) arrived under the old name only;of two files written or deleted back-to-back in one directory, only the first arrived.
Those changes reached the vault only when something ran a full pass (clawmem update, the
reindex tool). For a Claude Code auto-memory collection that was nearly every memory file the
agent wrote.
The watcher no longer relies on the name alone. It keeps a listing of each watched directory: every
file it acts on, with its inode, size, mtime and ctime. Every event, whatever name it carries,
schedules one rescan of its directory debounceMs (2 s) later; a later event does not push a pending
rescan back, so a busy directory cannot starve it. The rescan compares each file with the listing as
it stands at that moment and hands each one that appeared, changed or disappeared to the same
per-file debounce and routing an event goes through. A file whose own timer is already pending is
left to that timer, and every delivery records the file's state in the listing, so a rescan never
re-delivers a state that was already delivered, whether an event or an earlier rescan delivered it.
What changed
src/watcher.ts: the listing and the rescan. Each directory is listed before its watch starts and again right after; a change between the two listings is delivered, so a save that lands while the watch starts is not lost. Collection paths that overlap, or name one directory in two spellings (/notesand/./notes), share one listing and one rescan per directory. A rescan reads the directory asynchronously, then looks at 256 entries at a time and yields between batches, including when it checks the files it no longer finds. For a directory of 10,000.mdfiles, the event loop (which also serves the vector daemon) stalled at most about 3 ms during a rescan, against about 32 ms for one synchronous pass. On a host watching 15,274 directories holding 17,977.mdfiles, the two startup listings took about 0.3 s, next to a directory walk of 7 s or more.close()also cancels pending rescans, and no watch callback schedules work after it.Docs:
docs/troubleshooting.md(a new Indexing entry; the editor-autosave note),docs/guides/upgrading.md,AGENTS.md,SKILL.md.
Verification
tests/unit/watcher.test.ts adds fourteen tests. Eleven drive the real watcher. Six check delivery,
each exactly once under the file's own name: an atomic save that replaces a file and one that
creates it, a rename inside a directory (both names), two files written back-to-back, two files
deleted together, and an atomic save in a subdirectory. Three check that nothing extra is delivered:
a change the event stream reports by name is delivered once; files the watcher ignores, hidden files
and untouched files are not delivered; close() cancels a pending rescan. Under Bun 1.3.14 the six
delivery tests fail against the v0.40.1 watcher and the other three pass; under Bun 1.4.2 all nine
pass against both. Two more check that an atomic save in a directory two collection paths share
(overlapping, or one spelled with /./) is delivered once. Three pin the startup listing: which
files it holds; appeared, changed and removed files; a directory that is gone.
tests/integration/cmdwatch-atomic-save.integration.test.ts runs the real clawmem watch with a
Claude Code auto-memory collection: a file created by an atomic save, the same file replaced by one,
and two files rewritten back-to-back all reach the index with their new titles; against the v0.40.1
watcher under Bun 1.3.14 the first step never indexes. Full suite: 3079 pass / 0 fail; tsc unchanged.
Cross-model adversarial review (codex / GPT-6, one pinned session) took four turns and eleven findings,
nine Medium and two Low, all fixed. They included a rescan that held the event loop on a large
directory, a save missed in the moment a watch starts, a timer firing mid-rescan and overlapping
collection paths each re-delivering a file, mass removals that skipped the batching, and a claim
that a same-size rewrite inside one timestamp tick was covered (it is now a stated limit). It cleared
at turn 4 with zero remaining findings.
What didn't change
On Bun 1.4.0 and later the events already carry the right names: rescans find nothing more, and each change is still indexed once.
The per-file debounce, the pre-check and routing (
watchTargets), the directory cap and which directories are watched. Directories created after the watcher starts are still not watched until it restarts.Each file a rescan delivers starts an index pass of its collection, as each delivered event always has: a burst of N changed files costs N passes, the count Bun 1.4.0 and later already produced. On older Bun, where the fold hid most of a burst, that is new load after a large checkout or copy into a watched directory.
A rewrite that keeps a file's size and lands within one filesystem timestamp tick of the watcher's last look at it leaves inode, size, mtime and ctime all unchanged, so no rescan can see it. On Bun before 1.4.0 such a write, if its event is folded away, waits for the file's next change or a full pass.
Nothing to migrate. Restart the watcher, then run
clawmem updateonce to index anything the old watcher missed. Upgrading Bun to 1.4.0 or later also fixes the event names for earlier ClawMem versions.
v0.40.1 — the watcher re-indexes every collection it watches, and its directory cap is a setting
clawmem watch checks each changed .md file against its collection's pattern before it opens
the database, so a broad collection path with a narrow pattern does not start an index pass on
every edit under the tree. That pre-check read the directory part of a pattern as literal text
(relativePath.startsWith("*/memory/**/")) and split a brace list by stripping only a leading
{ and a trailing }. Two pattern shapes could never pass it:
a wildcard in a directory part, such as
*/memory/**/*.md(a Claude Code auto-memory collection rooted at~/.claude/projects);a brace list followed by a suffix, such as
{README,guide}.md, which split intoREADMEandguide}.md.
The watcher dropped every change event for those collections without a log line, so edits and
deletions reached the vault only when something ran a full pass (clawmem update, the reindex
tool). On the host where this was found, 774 indexed documents in five collections were never
re-indexed on change.
Routing had a gap of its own. An event went only to the collection with the longest matching
path, found by a string-prefix test. An overlapping outer collection (**/*.md around an inner
notes.md collection) therefore missed every file the inner pattern rejected, a sibling that
shares a name prefix (notes-archive/ beside notes/) was routed to the wrong collection, a
collection rooted at / lost the first character of every path, and a collection whose configured
path was not normalised (/notes/./x) received no events at all.
The pre-check now matches the way an index pass scans: matchesCollectionPattern in
src/indexer.ts uses the indexer's own brace expansion and Bun.Glob, so the watcher never skips
a file indexCollection would take. Files the pattern cannot match are still skipped before any
database access. An event now reaches every collection whose path contains the file and whose
pattern can match it (watchTargets), with each relative path computed by path.relative.
The release also documents the watcher's directory cap and makes it a setting. At startup the
watcher walks each collection path and watches every non-excluded directory, one OS watch each,
up to 500 per collection path. The cap was a constant that only a startup warning mentioned, and
the warning blamed file-descriptor exhaustion. On Linux, Bun keeps every watch on one inotify
descriptor; the limit that matters is the kernel's per-user fs.inotify.max_user_watches, which
every process shares. CLAWMEM_WATCH_MAX_DIRS now sets the cap. The default stays 500, so nothing
changes unless you set it. Past the cap the warning says how many directories it watches, that
changes in the others wait for the next full index pass, and which setting raises the cap.
What changed
src/indexer.ts:matchesCollectionPattern(pattern, relativePath), built from the indexer'sexpandBracesandBun.Glob(each alternative is normalised, so./and//segments and a trailing/match like the normal form; a pattern that is not relative to the collection root defers to the index pass);pathWithin(root, fullPath); andwatchTargets(collections, fullPath).src/clawmem.ts:cmdWatchre-indexes each collectionwatchTargetsreturns; no string-prefix gate runs before it, and the beads branch finds its collection withpathWithin. A pattern naming a single file (notes.md) now passes only that file at the collection root, as the index pass does; the old check also passed a same-named file in any subdirectory, which started a pass that indexed nothing.src/watcher.ts:DEFAULT_MAX_WATCH_DIRS(500) andresolveMaxWatchDirs()readCLAWMEM_WATCH_MAX_DIRS. Unset or empty means 500; any other value that is not a positive integer falls back to 500 with a warning line. The cap warning names the setting.Docs:
docs/reference/configuration.md(a new File watcher section),docs/troubleshooting.md(the pre-check and directory-cap entries),docs/guides/systemd-services.md(a drop-in example),docs/guides/upgrading.md,AGENTS.md,SKILL.md.
Verification
tests/unit/watcher.test.ts (16 tests) covers the pre-check (both broken shapes, unnormalised and
out-of-root patterns, non-matching paths still skipped, the shapes that already worked), the routing
(an overlapping outer collection, a name-prefix sibling, a / root, unnormalised collection paths,
a collection without a pattern) and the cap
setting (the default, valid, empty and invalid values, and a watcher capped at 3 that names the
setting in its warning). Against the v0.40.0 pre-check logic, the two broken-shape tests fail and
the others pass. tests/integration/cmdwatch-precheck.integration.test.ts runs the real
clawmem watch against a temp vault with the three collection shapes (one configured with an
unnormalised path) and checks the log lines and the indexed rows; against the v0.40.0 cmdWatch it
fails. On the host where the
bug was found, the new pre-check passes every indexed document; the old one dropped 774. Full suite:
3064 pass / 0 fail; tsc unchanged.
Cross-model adversarial review (codex / GPT-6, one pinned session) took five turns and fourteen
findings, seven Medium and seven Low, all fixed. They included the untested call site, an event
reaching only the longest matching collection path, a string-prefix gate ahead of the routing,
unnormalised patterns and collection paths that still lost events, and a file-descriptor diagnosis
that could miss watch exhaustion. It cleared at turn 5 with zero remaining findings.
What didn't change
The default cap (500), and which directories the watcher walks and excludes.
Index passes: a collection indexes the same files as before. Only which change events reach a pass is different.
Nothing to migrate. Restart the watcher to pick up the fix; one
clawmem updateindexes anything the old pre-check skipped.
v0.40.0 — after a compaction, a session gets back its own pre-compaction state and nobody else's
Through v0.39.1 the pre-compaction state was one file per project directory. precompact-extract
wrote precompact-state.md into Claude Code's auto-memory directory
(~/.claude/projects/<project>/memory/), and postcompact-inject read it back on every session
start in that directory, with no session key, no age check and no check of why the session
started. So:
a session received the last compaction of whichever session in the same project directory had compacted most recently, including one that ran concurrently in another terminal;
a fresh start, a resume or a
/clearreceived it too, because the installer putpostcompact-injectunder SessionStart matcher"";a PreCompact that extracted nothing left the older file in place, to be replayed at the next compaction;
the block was injected unfiltered, under the heading "authoritative", which told the model to prefer it over its own summary.
The extraction itself was also wrong. The "last user request" was the last entry with the user role, which in an agentic session is almost always a tool result, and decisions were mined from tool input and output as well as prose. And PreCompact re-indexed a collection from inside the hook, so the state files were indexed and surfaced in search as ordinary memories.
What changed:
The state belongs to one session. PreCompact stores it as that session's row in a new vault table,
compaction_state. The SessionStart that follows the compaction (sourcecompact) takes it once: reads and deletes it in one statement. A state older than 15 minutes is never injected, and a start for any other reason injects nothing. The block's recent decisions, antipatterns and vault context still come from the whole vault, as before; only the pre-compaction state is per session.A failed PreCompact leaves nothing to replay. Before it opens the vault, PreCompact registers its attempt in a small database beside the vault (
<vault>-compaction.sqlite), and the SessionStart takes only the row carrying the registered attempt. A PreCompact that fails after registering, with a busy vault, an unreadable transcript, nothing to extract or the host's timeout, leaves no snapshot to inject, and an older PreCompact that finishes after a newer one started stores nothing, whether it resumes before or after the newer one stored its state. A SessionStart that runs while its PreCompact is still extracting retires that attempt, and one whose read of the vault waited while a newer PreCompact of the session registered injects nothing.The block is data. It is framed as notes extracted by pattern matching, to be checked before acting on them, and every field (captured text, vault titles, paths, snippets) is filtered for injection, flattened to one line, bounded, and cannot open or close a tag.
The request is what the user typed. A classifier separates typed prompts from tool results, harness records (command wrappers, local command output, task notifications, system reminders) and meta entries, and it looks back up to 2000 entries for the last one. Decisions and open questions come from prose only.
readTranscript's output is unchanged.No re-index from PreCompact. The hook writes nothing into Claude Code's memory directory and indexes nothing.
clawmem setup hooksinstallspostcompact-injectin its own SessionStart group, matchercompact, and removes ClawMem's hooks handler by handler, so another tool's hook in the same group survives an install or a--remove. The manual JSON in the setup guide now has the{matcher, hooks}shape Claude Code expects, and the transcript-path example uses.transcript_path.The old files stay out of retrieval and enrichment. Search and retrieval never return an indexed copy of the old
precompact-state.md(recognised by the header the old versions always wrote, never by name alone), whoever indexed it, including an older ClawMem still running against the vault. The check runs inside every query that finds documents nobody named: the keyword, vector, graph, entity and causal routes, timeline neighbours, review reminders, evolution history, a glob or path-suffix match inget/multi_get/ theclawmem://resource, the did-you-mean list, the target search ofmemory_pin/memory_snooze/memory_forget, and the REST export. No automatic enrichment or embedding reads a copy either: A-MEM notes, links and evolution, entity extraction, graph building, consolidation, deduction, conversation synthesis,clawmem embedand the doctor's vector sampling all skip it. So there is nothing to migrate first and no window while one runs. A get (or a lifecycle tool) by its exact path or docid still reaches it. The indexer skips the files and deactivates each indexed copy it finds. A file with that name and your own content is an ordinary document.Notes a copy shaped are rebuilt. Older versions let a copy feed A-MEM evolution, so another session's text could end up in a note's A-MEM summary and be carried forward. The first writable open clears each such note, the light-lane backfill (
CLAWMEM_ENABLE_CONSOLIDATION=true) rebuilds it from the note's own text, andmemory_evolution_statusshows areset:entry where the tainted entries were. A note indexed from a file is also rebuilt when the file changes or byclawmem reindex --enrich; one that hooks or the API wrote stays without an A-MEM note until the light lane runs (search and injection never use one). No prompt reads such a note meanwhile. A note an older ClawMem still running evolves after the upgrade gets the same treatment, whether or not a copy ever touched it, since an older process reads notes unguarded: this version stamps every evolution entry it writes, and an older one cannot.REST
/exportleaves the copies out by default and says how many (legacy_snapshots_excluded);?full=trueincludes them (every active document).getandmulti_getresolvecollection/pathexactly before they try the text as a path suffix, so a display path no longer depends on the suffix fallback's first match.clawmem doctorflagspostcompact-injectunder any matcher butcompact, lists the old files (red when one was written after the upgrade, which means an older ClawMem process is still running against the vault), and lists indexed copies that are still active.OpenClaw takes its fallback session id from the transcript file's stem, and skips a stem it cannot split with certainty (one containing
-topic-).
All of this describes an upgraded process. An older ClawMem still running against the vault keeps its own behaviour, the leak included, until it is upgraded or stopped: upgrade every process that shares the vault.
The state still depends on Claude Code sending the same session_id to PreCompact and to the
SessionStart that follows it, which is what the field means on both events. One case stays
open by nature: a PreCompact whose disk refuses both the registration and the vault write cannot
mark anything stale, so an earlier state of that session that nothing took can still be injected
within its 15 minutes.
Verification
Nine test files, tests/unit/compaction-*.ts: 120 tests. compaction-cli.test.ts runs the two
hooks through the real clawmem hook dispatcher, one process per hook, as the host does;
compaction-mcp.test.ts drives the real MCP server, resource and REST routes;
compaction-enrichment.test.ts runs the enrichment pipelines with a model stub that records every
prompt; and compaction-embed.test.ts runs clawmem embed against an embedding server that records
every input. Of the tests that drive public entry points (the hook dispatcher, setup hooks,
doctor, the OpenClaw engine, the MCP server and REST routes, and clawmem embed), 34 of 38 fail on
the v0.39.1 code as each defect predicts; the 4 that pass there are controls. The unit tests of the
new state store, registration, transcript classifier and A-MEM repair need modules v0.39.1 does not
have, so they cannot run there. Across the release, 66 targeted reversions of its behaviour each
make a test fail.
Full suite: 3047 pass / 0 fail; tsc reports the same 85 errors as v0.39.1. Cross-model
adversarial review (codex / GPT-6, one pinned session) took fifteen turns and 75 findings, 22 of
them High, all resolved before release; three were questions of scope that the maintainer decided.
It cleared at turn 15 with zero remaining findings.
v0.39.1 — collection and context edits keep the comments in config.yaml
Every command that writes ~/.config/clawmem/config.yaml re-serialised the parsed config with
YAML.stringify: clawmem collection add and clawmem collection remove, and the rename and
context writers behind the store's collection-rename and context-delete paths. Each write
silently dropped every comment, blank line and quoting choice in the file, with exit 0 and no
warning. Those comments are often the only record of why an entry looks the way it does. The
config has no exclude key, so a collection whose pattern lists its subtrees instead of **/*.md,
to keep vendored clones out of the vault, is explained in a comment or not at all — and a later
tidy-up that cannot see the reason widens the glob.
The writers now edit the parsed YAML document in place. An edit rewrites only its own lines;
comments, blank lines, quoting and key order everywhere else come back as they were. Removing a
collection removes the comment lines directly above it; a comment that a blank line separates
from the entry, such as a section header, stays where it was, and so does a comment at the end
of the collections: line. Collection names that YAML reads as numbers (2024:) are matched as
the names they are. YAML aliases keep their values: when an edit changes or removes what an alias
names, that alias becomes a copy of it first, and aliases an edit does not reach are written as
they were. Several aliases to one mapping or list share one copy, so they stay one value. An alias
inside the node it names (an entry that refers to itself) is not copied; it goes on naming that
node, edit included.
Before it writes, each edit reads back the text it is about to write and checks that the effective
config changed only where the edit names, and as asked. Where the file uses a YAML feature an
in-place edit cannot keep, such as a collection that only a merge key (!!merge <<:) provides, the
command stops with an error naming what would have changed, and the file is left untouched.
Two smaller changes come with it:
clawmem collection addwith a name that already exists updates that collection's path and pattern in place and keeps itsupdatecommand. It used to rebuild the entry from path, pattern and context and dropupdatewithout a word.A write no longer copies the lifecycle defaults into the file.
lifecyclestays as you wrote it; ClawMem still applies the defaults when it reads the file, so the effective policy is unchanged.
A few cosmetic limits remain, all from the YAML library's printer, and none of them loses data or
a comment. Indentation is normalised to two spaces, as before. A comment on the last line of the
file gains a blank line above it on the first write. An anchored block whose first comment
follows a blank line has its anchor moved onto a line of its own. Flow collections ([a, b]) are
all written with the spacing most of the file's flow collections use.
Verification
tests/unit/collections-config-comments.test.ts (59 tests) states the exact file each writer
must produce from a commented config: add, re-add, remove (first, middle, last and only entry,
with and without a blank-line header), rename, a numeric-looking name, an empty {}, a missing
file, an end-of-file comment, a parse error, and the three context writers; then anchors, aliases,
tags, alias and list keys, flow spacing, merge keys, self-references, and !!set and !!omap
values. On the v0.39.0 code 47 of the 59 fail; the 12 that pass cover behaviour this release
keeps. Full suite: 2927 pass / 0 fail. Cross-model adversarial review (codex / GPT-6, one pinned
session) took ten turns and twenty findings, five of them High, all fixed except one Low accepted
as the flow-spacing limit above. They included aliases an edit changed or left dangling, explicit
tags kept on new values, key aliases and list keys renamed, a self-referencing alias copied
without end, and a collection that only a merge key provides written half-made, which led to the
read-back check. The last two rounds found that check refusing correct edits, of self-referencing
files and of !!omap and !!set values. It cleared at turn 10 with zero remaining findings.
v0.39.0 — the OpenClaw plugin installs and runs on current OpenClaw, and forgotten counts only forget
OpenClaw added three checks between April and May 2026 that each switched off part of the ClawMem plugin without an error at install time, and the plugin had no time budget of its own for the context-surfacing hook. This release answers the three GitHub reports that followed (#26, #27, #28) and carries one reporting-contract fix for the lifecycle counts.
OpenClaw plugin: installs, registers and keeps its hooks on OpenClaw 2026.5+ (#27)
The manifest declares the five agent tools under
contracts.tools. From OpenClaw 2026.5.2 the registry rejectsregisterToolfor any undeclared name, and the plugin kept running hook-only. Thanks @cleverark (PR #27) for the manifest change and the docs section this release builds on.clawmem setup openclawinstalls a compiled copy. From OpenClaw 2026.5.3,openclaw plugins install --forcerefuses a package whose entry isindex.ts, so setup bundles a Node-targetdist/index.js, pointsopenclaw.extensionsat it, and installs that staged copy. Nothing generated is committed.Capability consent. OpenClaw 2026.5 asks for consent to a plugin's declared capabilities on every local install. Setup prints what ClawMem declares and passes
--accept-capabilitiesonly after an interactive yes or withclawmem setup openclaw --accept-capabilities(alias--yes/-y); a non-interactive run without it stops and says so.Setup writes the config a working install needs and reads each value back: the absolute
clawmemBin;plugins.entries.clawmem.hooks.allowConversationAccess=true(from OpenClaw 2026.4.23 a plugin OpenClaw did not bundle may not registerbefore_prompt_buildoragent_endwithout it); andplugins.slots.memory=clawmem(since the September 2026 main branch an unselected memory plugin still loads but loses its memory runtime). A write that does not read back fails setup with a non-zero exit. Setup also warns onallowPromptInjection=falseand on an operator hook-timeout policy below the plugin's own timeout.--gateway-user <name>for system-service installs: setup checks that the installed files are owned by that user or root and not world-writable, that the user can read them through every directory on the way (symlink targets included), and that it can run theclawmembinary, all judged from its uid and supplementary groups. A missing manifest orpackage.jsonfails the check too. Setup exits non-zero when any check fails.OPENCLAW_PROFILEreaches OpenClaw: setup passes it to everyopenclawcommand as--profile <name>(the variable alone selects nothing in OpenClaw) and checks it against OpenClaw's profile-name grammar before deriving any path from it,--removeincluded.The installed root is read from
openclaw plugins inspect clawmem --json; an inferred root is used for diagnostics only.A replaced install is parked first and put back if the replacement fails, on the delegated, CLI-absent and
--linkpaths. A failed restore names the parked copy (clawmem.old-<pid>) and themvthat brings it back.Setup records
clawmemBinonly as an absolute path to a regular file the installing user can execute (a bareclawmemis resolved throughPATHfirst) and stops otherwise; the plugin refuses a configuredclawmemBinthat is missing, not a regular file, or not executable.
OpenClaw plugin: one time budget for the context-surfacing hook (#28)
The new plugin config hookBudgetMs (default 6000 ms, 1000 to 25000) is the hook's wall-clock
budget. The plugin passes it to the hook as CLAWMEM_HOOK_BUDGET_MS, which the v0.38 hook
schedules its legs against; kills the hook 2 s after it; and registers before_prompt_build with
OpenClaw 2 s after that, so the three limits stay in that order. An operator hook-timeout policy
below the registration value still fires first, and setup warns when it finds one. 25000 is the
hook's own ceiling (MAX_LEG_BUDGET_MS): the hook refuses to run above it, so the plugin clamps
to it rather than pass a value the hook would refuse. Timeout messages name the hook, the profile
and the budget. At profile: deep without an LLM or reranker endpoint, the plugin logs one
warning at registration that names the missing legs.
node-llama-cpp ^3.20.0 (#26)
node-llama-cpp moved to ^3.20.0 (llama.cpp b10361) and bun.lock was regenerated. A source
checkout on macOS 26.6 with an M5 Pro no longer fails with
ggml_metal_library_init_from_source: error compiling source. In-process Qwen3 reranker scores
are now the model's probability; 3.15.1 applied a second sigmoid that squeezed every score into
about 0.50–0.73. The local rerank cache is namespaced by a new score revision, so scores cached
under 3.15.1 are never reused. Remote rerank caches go cold once as well: until this release the
in-process fallback wrote its scores under the remote endpoint's namespace whenever the endpoint
failed, so a v0.38 remote cache can hold local, squeezed-scale scores labelled as the endpoint's.
The fallback now caches only in local mode, and remote namespaces carry a revision that makes
those entries unreachable. The first reranked queries after upgrading re-score cold. On CUDA the
first in-process embedding after the upgrade compiles its kernels once (about ten seconds on a
GTX 1080 Ti); run clawmem embed once after upgrading to pay that outside a prompt.
Changed — forgotten counts only forget (a reporting-contract change)
clawmem lifecycle status, thelifecycle_statusMCP tool,GET /lifecycle/statusandclawmem curatereportforgottenas the number of documents deactivated by forget (deactivated_reason = 'forget'). It used to count every inactive document withoutarchived_at, which also took in documents whose file disappeared and documents deactivated before v0.31.0 recorded a reason — so on an existing vault the number usually drops. Nothing in the vault changes; the old count was mislabelled.New: every inactive document broken down by deactivation reason —
absent,forget,archive,unknown_legacy(no recognised reason and noarchived_at; in practice deactivated before v0.31.0, cause unrecoverable, so not guessed). Exhaustive and disjoint. CLI and MCP printDeactivation reasons: absent N, forget N, archive N, unknown-legacy N;GET /lifecycle/statusaddsdeactivation_reasons; the curator report addshealth.deactivationReasons. No migration.
Docs
Upgrading guide: a hook that starts while an upgrade is still replacing files can load a mix of old and new modules and fail; the failure is non-blocking, so that one prompt runs without ClawMem context (or that turn-end extraction is skipped), and the next invocation loads the new code.
The CLI reference documents the new
setup openclawflags andOPENCLAW_PROFILE; troubleshooting adds the Apple Silicon Metal compile error, the hook timeout message and the conversation grant.
Verification
Live, on the pre-rebase branch (2026-09-09):
clawmem setup openclaw --accept-capabilities --gateway-user <user>against a real OpenClaw 2026.9.2 in an isolated state directory. The compiled stage was accepted, the memory slot moved from memory-core, all three config values read back,openclaw plugins inspect clawmem --jsonreported the plugin loaded and activated, and the gateway logged the registration (kind=memory,hookBudgetMs=6000), the five agent tools and the REST service.Full suite: 2868 pass / 0 fail · the five OpenClaw suites: 200/200 ·
tsc --noEmit: 85 errors, all pre-existing (the same set as v0.38.1 with the two handed-over commits), none new.New locks: the plugin's default, minimum and maximum budget equal the hook's, the hook's parser accepts every value the plugin can pass unchanged, and the manifest carries the same range (four of these fail against the former 60000 cap); the local rerank namespace carries the score revision (fails against the v0.38 key); an attested endpoint that fails leaves nothing under its key and the next healthy call scores live (fails against the v0.38 write rule); read and traverse access for the gateway identity is judged from mode bits along the kernel's own walk, symlinks included, and an installed copy missing its manifest fails
--gateway-user; aclawmemBinwithout an execute bit is refused at setup and at plugin start.Cross-model adversarial review (codex): the OpenClaw work cleared after seven turns; this release's rebase, budget ceiling and rerank-cache changes took four more turns on the same session and seven findings, all fixed (local fallback scores cached under a remote namespace; the gateway user's read access, including symlinked and slash-terminated paths; the recorded
clawmemBin; two overstated release-note sentences), and cleared at turn 11. The lifecycle change cleared its own review in the first round, and its O1 clock and seam audits found nothing.
What didn't change
No schema change and no migration. Hooks, MCP tools and the REST API behave as in v0.38.1 apart from the lifecycle counts and the one-time cold rerank caches above.
v0.38.1 — the watcher now logs the version skew it refuses
v0.38.0's vector wire refuses mixed builds in both directions, but only one direction was
visible. A v0.38 hook talking to an older watcher prints a once-per-process warning naming the
socket. A pre-v0.38 hook talking to a v0.38 watcher is refused as version_skew, and that old
hook maps the error to its generic FTS fallback — silent by default; even its opt-in
CLAWMEM_VEC_TIMING trace shows only path=error. The watcher said nothing either. A hook and a watcher running from different installs therefore lost every vector leg
with no trace on either side — surfacing kept working on keyword matches, so it read as weaker
recall rather than as a fault.
The watcher's vector daemon now logs the first refusal:
[vec-daemon] refused a request from a pre-v0.38 client on <socket> (absolute deadlineMs → version_skew) — its vector legs degrade to FTS until it runs this build (usually the hook, from another install); further refusals are not loggedIt logs once per daemon (the watcher hosts one per run), never per request: an old hook sends a
refused request on every prompt. journalctl --user -u clawmem-watcher.service | grep version_skew finds it; the fix is to run the hook from the same install as clawmem watch and
restart the watcher (troubleshooting). The daemon runs inside the
watcher, so the line appears only once the watcher itself runs v0.38.1.
Verification
A new unit lock sends three pre-v0.38 requests through a live daemon socket and asserts exactly one log line, naming the socket; a ping and a served request add none; a restarted daemon logs its own first refusal. A second lock makes the log sink throw on the first refusal: the daemon's entire reply is still the single refusal frame, and the line is written on the next refusal instead, so a line the sink refused does not count. Each of these mutants fails a lock: the once-guard dropped, the log call dropped, the flag hoisted to module scope, the flag set before the line is written. Full suite: 2787 pass / 0 fail. Cross-model adversarial review (codex / GPT-6, the arc's pinned session) caught the log-sink failure path in its first round and cleared the second with zero remaining findings.
The release's suite run also caught a pre-existing test flake. A hydrated-projection test capped
a leg's recorded 400 ms budget at exactly 400, but the budget is the difference of two
performance.now() instants: when a leg starts within 400 ms of a power-of-two millisecond of
process uptime, the deadline's addition rounds and the budget reads 400.0000000000582. The bound
now allows float rounding, as the wall-jump lock's already did; the recorded evidence is
unchanged.
What didn't change
The wire contract is byte-identical: a refused request still receives exactly
{"error":"version_skew"}, decided by field presence before anything is decoded, and nothing is
scanned. The hook is unchanged, including its once-per-process warning for the opposite
direction. No schema, configuration or CLI change.
v0.38.0 — the context-surfacing hook ranks, admits, and bookkeeps on one honest key
The hook's ranking pipeline had accumulated incomparable signals: raw cosine and BM25 scores sorted against each other, composite-score admission that rejected on-topic documents while admitting junk, a topic boost and metadata reorders layered on top of the sort, and post-injection SQLite bookkeeping riding the prompt-latency path. This release rebuilds the pipeline around a single ordering authority, measured end to end by a judged evaluation (labeled gold cases replayed through the real handler) that ships as part of the release.
One channel-aware ordering key
Every retrieval leg — BM25, vector, file-aware, the gated prior-turn leg, and deep expansion variants — contributes a ranked lane; weighted reciprocal-rank fusion produces one key per candidate: a band (0 = supported by the current turn's own lanes, 1 = discounted-lane-only survivor) and a scale-free fused mass. That key decides pool membership (under an expansion mass cap + protected current-class slots, so recall hints can never outvote the user's actual question), the final injected order, and — new in this release — admission. Discounted-only candidates order strictly below every current-supported one. No mixed raw-score sorts remain, and no metadata reorders: the spreading-activation and memory-type-diversification stages are deleted, and pins/recency/quality multipliers can no longer change hook membership or ordering — composite retains tier sizing, so they still influence snippet depth (HOT/WARM/COLD). Co-activation is absent from the hook entirely. (All of it still acts on the composite MCP surfaces.)
Relevance admission, with honest abstention
Keep/drop is judged on the same key that orders: a relative floor (≥ 50% of the top
candidate's fused mass) inside band 0, and query-level abstention when there is
nothing defensible to inject — no-current-support (band 0 empty, unless the
anaphora gate certified the prior turn as the only signal) and degenerate-basis
(FTS agreed on nothing — the vector-only junk signature of gibberish prompts). The
pre-existing composite gate survives solely as the eval control arm behind
CLAWMEM_ADMISSION_POLICY=composite.
The deep rerank lane earns its influence
An applied rerank needs full candidate coverage (a partially-covered pool is never
partially reordered) AND a discriminating score set — the per-request degeneracy
gate (CLAWMEM_RERANK_DEGENERACY_GATE, default on) discards collapsed or near-constant score sets instead of trusting their arbitrary order (the 0.05 spread floor is calibrated from the zerank baseline and validated by the judged eval; another provider may need its own calibration). A passing rerank
joins the final order as one more rank-fused lane (CLAWMEM_RERANK_LANE_WEIGHT,
default 1.5) — mass within the bands, never a band elevation. Remote rerank scores
are now cached only under an attested provider identity: clawmem rerank-health
fingerprints what the endpoint actually serves (7-day expiry, failed probes revoke),
so cached scores are namespaced to the attested provider. A same-URL swap nobody re-probes is honored until the next rerank-health refresh or the 7-day expiry — which is exactly why attestations expire instead of being trusted indefinitely. The rerank
request revision bumped (RERANK_REQUEST_REV=2), invalidating prior cache entries
once.
Deadlines that hold
CLAWMEM_HOOK_BUDGET_MS (default 6000) is the hook's authoritative internal budget;
every in-handler deadline derives from it, with a 500ms finalization reserve — a measured, invariant-audited margin for payload assembly, not an unconditional guarantee (the write path that could stall left the handler entirely, below). Expansion carries a real transport abort (an abandoned
promise no longer holds the process past its budget), and clawmem setup hooks
derives the host timeout from the budget (≥ 1.5s startup + budget, never reducing a
larger existing value) while clawmem doctor checks the inequality.
Scope of the vector deadline — daemon-backed deployments. The one deadline the handler
cannot enforce by itself is the vector leg's: the sqlite-vec MATCH is synchronous, and a
Promise.race timer on the same event loop cannot fire while it runs. The profile's vector
timeout (900ms balanced, 2000ms deep) is therefore authoritative when the watcher's vector
daemon serves the vault (clawmem watch — the scan runs off the hook's loop and the timer
can actually fire); without the watcher the hook still works, but a cold multi-GB scan blocks
the handler for the scan's full duration and the budget is a target, not a bound. The
release's latency evidence was gathered under exactly that daemon-backed topology: the hook
replay-eval spawns a dedicated vector-daemon child on its working copy (the watcher's
steady-state prewarm performed before readiness, every vector leg daemon-required, daemon
loss refuses the run) and records the protocol in the run identity (vector_exec), where it
is strict across baseline, pair and replicated comparisons; an in-process replay is recorded
as such and its latency axes are unmeasured. Making the vector path structurally bounded
without a watcher is future work, not a claim of this release. Fixed alongside: a stale
daemon socket left by a crashed watcher silently prevented the next watcher from ever binding
its daemon (the liveness probe threw on a missing socket handler and the bind was skipped) —
the hook then ran in-process, unbounded, with no diagnostic; the probe is fixed and the
stale-socket bind is regression-tested. Because the contract is scoped to daemon-backed
deployments, the prerequisite is checkable: clawmem doctor now verifies the vault's
daemon by a real round trip, and clawmem vec-daemon-health (exit 0 only for an attested live daemon on the vault's
socket; live-raw and live-legacy prove a listener but not the contract, and exit 1) is
the scriptable gate the shipping preflight uses.
In the replay-eval, daemon ownership is re-verified before and after every rep, a
steady-state prewarm that had no vector payload to warm refuses vector-exercising runs,
and an in-process replay's budget gate is reported as unmeasured (raw timing kept as a
diagnostic) rather than as a pass or an overrun.
Deadlines are monotonic now (O1). Every deadline in the hook used to be an absolute
wall-clock instant compared against Date.now(), so an NTP step moved every deadline at once
and silently falsified the timing evidence the trust gate scores (measured on the release
host: a time daemon stepping +3.6 s every ~36 s produced four refused draws with multi-second
"overruns" that never happened). The handler now derives every deadline from ONE monotonic
anchor at entry; the daemon wire carries a relative remaining budget (remainingBudgetMs,
sampled immediately before the write) instead of an absolute deadline, and the daemon's own
deadline is advisory (check-before / check-after each synchronous phase — never cancellation);
expansion and rerank transports are cut by monotonic signals; the Stop hook's phase floors are
monotonic too. A pre-O1 request carrying deadlineMs is refused as version skew by field
presence, and a pre-O1 daemon that ignores the budget is classified skew — the leg degrades
to FTS with a once-per-process warning naming the socket (restart clawmem watch). The
mismatch runs both ways, so upgrade the hook and the watcher in the same step: a pre-v0.38
hook talking to a v0.38 watcher loses its vector legs to FTS silently. Only the
clock module samples a platform clock; two static audits (a raw-clock ratchet and a typed seam
audit over branded MonoDeadline / DurationMs / EpochMs values, both at zero debt) keep it
that way. The timing evidence changed with it: every rep's per-leg record is persisted
(vector_leg_records: monotonic over_ms, the span on both clocks with clock_skew_ms
exposing a realtime step, an orthogonal terminal kind × execution path, and a timing class
against the frozen 150 ms tolerance — never re-fitted). The contract is identity: the run
records deadline_protocol: "monotonic-relative-v1", the daemon advertises deadline-rel-v1
beside hydrated-v1, clawmem doctor / clawmem vec-daemon-health require both for the
authoritative live tier, and every comparison surface (baseline, pair, replicated members)
fails closed on a report without it. CLAWMEM_HOOK_BUDGET_MS gained a maximum of 25000
(the wire ceiling): larger values are refused by the hook, by clawmem setup hooks and are
reported by clawmem doctor; the fallback and clamp behaviours below the maximum are unchanged.
Bookkeeping left the hook
After the payload is assembled the hook does zero SQLite/filesystem work in normal production mode (the one disclosed exception: the diagnostic CLAWMEM_SURFACING_TRACE=1 trace persist). Turn
alignment became a single early fail-closed context_usage row at retrieval
commit — no row, no injection, so injected context is never untracked and prompt
history survives deadline skips. Recall events, injected-paths fill-in, and
secondary-vault mirrors are handed to a detached clawmem spool-ingest child over a
bounded pipe (250ms flush race; losing the race costs only that turn's optional
learning data) and applied by a claim-by-rename drainer with per-row idempotency
keys — crash-and-retry can never double-count. clawmem spool-drain applies pending
jobs manually.
Session focus narrowed to presentation
A focus topic now steers snippet selection only. The 1.4×/0.75× post-composite topic boost and the expansion/rerank intent threading are removed: a session preference must not change what surfaces or in what order, only which sentences of a surfaced doc are shown.
The judged hook eval harness
clawmem eval hook-run replays labeled UserPromptSubmit cases through the real
handler against a corpus snapshot: graded nDCG over the injected order, must-include
recall, must-not damage rate, abstention accuracy, prior-leg accuracy, latency, and a
hermetic invariant audit — with run-identity fingerprints (corpus, code, inference
topology, served-model probes), holdout-only acceptance gates, paired counterfactuals
(frozen expansion draws + per-case pre-treatment audits), registered treatments, an
experiment-pinned clock, and eval hook-aggregate for replicated-draw distributional
verdicts. CLAWMEM_SURFACING_TRACE=1 captures the same per-stage trace envelope
live into surfacing_diagnostics for post-hoc diagnosis.
Verification
Judged replicated A/B on a frozen corpus snapshot: 31 labeled cases (22 tuning, 9 holdout), five frozen expansion draws, each captured on the control arm (the pre-v0.38 composite admission) and replayed into the candidate (relevance admission), every candidate pair-gated at 31 valid pairs. Relevance admission raised graded nDCG from 0.528 to 0.830 and must-include recall from 0.50 to 1.00 in every draw (direction-stable across all five), with abstention and prior-leg accuracy at 1.00, no must-not hits and no timeouts; latency moved within noise (p50 ≈ 57 ms, p95 ≈ 1.8 s). Every run's trust gate passed under the daemon-backed protocol and the monotonic deadline contract: across 1,065 vector legs the worst overrun past its own deadline was 1.7 ms (tolerance 150 ms), and the slowest handler finished in 4.1 s of its 6 s budget. Two draw slots were redrawn under the pre-registered protocol after pair-gate refusals caused by transient host load (a control-arm vector leg cut at its deadline); the refused draws are kept as evidence, never reused.
Cross-model adversarial review (codex / GPT-5.x and GPT-6), pinned sessions: the ranking, admission, bookkeeping and hydrated-v1 work was reviewed turn by turn through the arc; the monotonic-deadline design went through thirteen revisions before implementation, and its migration cleared a four-round review to zero remaining findings (10→7→1→0). Wall-clock steps of ±5 s, injected in the hook process and independently in a daemon child, leave every client and daemon deadline decision unchanged; a mutation that makes the control clock follow wall time fails all six of those locks. Full suite at clearance: 2785 tests, 0 failures.
What didn't change
The MCP retrieval tools keep composite ranking — pins, recency, quality and co-activation
still act there; only the hook stopped ordering and admitting on it. Without a watcher the
hook still runs its vector leg in-process: it works, but that leg's timeout is a target, not
a bound. Defaults are unchanged: CLAWMEM_HOOK_BUDGET_MS 6000 and the profiles' token
budgets and vector timeouts; below the new 25000 maximum the budget's fallback and clamp
behave as before. Wall time still drives what is genuinely wall-clock — document ages,
cooldowns, leases, identifiers and timestamps — now read through the clock module instead of
Date.now(). Schema migrations are additive; no reindex, re-embed or graph rebuild.
Upgrade notes (schema auto-migrations, the one-time rerank-cache cold start, the
recommended setup hooks re-run, restarting the watcher together with the hook): docs/guides/upgrading.md.
v0.37.0 — a reachable-but-wrong inference endpoint now degrades instead of silently dying
Issue #24: when CLAWMEM_LLM_URL pointed at a port where an unrelated service answered
HTTP (a file browser squatting :8089), every generate() call failed silently and
permanently. HTTP errors deliberately never tripped the transport down-cache ("the
server IS reachable"), so the local in-process fallback never engaged — A-MEM enrichment
produced nothing for months while indexing kept reporting success. A dead port degraded
gracefully; a squatted one failed forever.
HTTP-shape down-cache
The single-error rule is unchanged — one 400/500 from a real server having a bad moment never costs it the GPU lane, and 429 (a healthy-but-limited endpoint) is never counted. What changed is the pathological tail:
405 / 501 trip the 60s cooldown immediately — a correctly-routed OpenAI-compatible route cannot return these; they are the signature of another service listening on the port. 404 is deliberately streak-based, not instant: cloud and gateway endpoints return 404 for an unknown model or deployment while the route itself is fine, so a mis-set
CLAWMEM_LLM_MODELmust not instantly cost the lane. A genuinely squatted port 404s every call and still trips within three.Any other non-2xx trips after 3 consecutive failures. A success resets the streak.
The trip emits one loud, actionable line naming the URL, the observed status, and the env var to fix — then the existing cooldown + fallback + notify-once machinery takes over. The transition is idempotent under concurrency: in-flight requests that return errors after the lane is already down are swallowed, so the line really is one line. The cooldown self-heals: a recovered server gets its lane back on the first attempt after expiry.
With local fallback permitted (the default), enrichment and query expansion degrade to the in-process model during the cooldown instead of dying. Under
CLAWMEM_NO_LOCAL_MODELS=truethere is still no fallback by policy — those deployments keep getting nulls, now with the trip line and the run-summary counter below instead of silence.The same trip protects the self-hosted embedding lane (
CLAWMEM_EMBED_URL, single and batch). Cloud embedding (API key set) is deliberately exempt: those lanes never fall back by design, and an auth/quota HTTP error must stay a per-call error rather than flip the vault onto a different local model mid-run.The judge lane is untouched:
generateJudgeChatkeeps returning typedhttpfailures to its caller and never trips the shared cache (judge-scoped instances are separate objects anyway). The reranker needs no HTTP trip — it already has shape-level protection (discrimination probe + degenerate-score fallback to RRF).Known limit, stated: a squatter answering 200 with a non-JSON body is not counted — the parse failure still surfaces as a logged per-call error. Non-2xx is the observed squatted-port signature and the conservative trigger.
Index-run summaries count note productivity
clawmem update, reindex, mine, the watcher, the MCP reindex and vault_sync
tools, and the REST /reindex response now report ✎stored/attempted notes whenever
A-MEM enrichment ran, and call out a gap explicitly (N produced nothing (LLM endpoint problem? run 'clawmem doctor')). This is the reporter's third suggestion: a run whose
every enrichment produced nothing must not end in an unqualified success summary, and
per-doc [amem] lines are too easy to read past. The metric is deliberately the NOTE
WRITE — the one phase every enrichment attempt runs — not whole-pipeline success: a
stored note whose later link phase failed still counts as stored (the failure keeps its
own log line), and an empty note whose entity phase happened to succeed still counts as
producing nothing.
clawmem doctor probes the LLM endpoint's shape
Reachability is not the same as serving chat completions. Doctor now POSTs a minimal
completion to CLAWMEM_LLM_URL and validates the response deeply (a non-empty choices
array whose first message content is a string — {"choices":[]} proves nothing),
honoring the same CLAWMEM_LLM_MODEL / CLAWMEM_LLM_NO_THINK normalization the runtime
uses. This closes the same "liveness ≠ correctness" gap the reranker discrimination probe
and the embedding geometry canary already close for the other two inference services. A
squatted port shows as ✗ LLM endpoint: … reachable but NOT serving chat completions; an
unset URL is a yellow note (the in-process fallback is a supported configuration).
No schema change, no migration, no new configuration. Behavior change: deployments whose
LLM endpoint persistently answers HTTP errors will now see the local fallback engage in
60s windows (with one explanatory line per trip) where they previously saw permanent
silent nulls — where local fallback is permitted; under CLAWMEM_NO_LOCAL_MODELS=true
the outcome stays null by policy and the trip line plus the note counters are the
improvement. IndexStats gains enrichAttempted/enrichStored (additive), and the
REST /reindex response carries the same two fields.
Verification
Cross-model adversarial pass (codex / GPT-5.x), one pinned session: four turns to zero
remaining findings (5→4→1→0). Review-driven hardening along the way: 404 demoted from
instant-trip to streak-based (cloud gateways legitimately 404 on an unknown model), the
trip transition made idempotent under concurrent in-flight requests, the doctor probe
extracted into a directly-testable function honoring the runtime's no-think/model
normalization, and the counters re-keyed to the note write with the remaining public
surfaces (REST /reindex, MCP vault_sync) brought into line. Production-boundary
tests drive real fixture HTTP servers through the probe taxonomy, the real
clawmem doctor CLI as a subprocess, and the real REST and MCP handlers for the
counters. Full suite at clearance: 2231 tests, 0 failures.
What didn't change
A single HTTP error still never costs the lane — the streak resets on success, 429
never counts, and AbortError/transport classification are untouched. Cloud embedding
keeps its no-fallback contract. The judge lane keeps returning typed http failures and
never touches the shared down-cache. The reranker keeps its existing shape-level
protection (discrimination probe + degenerate-score RRF fallback). The stdio MCP
transport is untouched. No new environment variables; the trip thresholds are design
constants.
v0.36.0 — the ranking pipeline and vault lifecycle become inspectable
Two read-only diagnostic MCP tools. Composite ranking and lifecycle state were previously observable only from the outside: you saw the final ordering and the final counts, and answering "why did this doc outrank that one" or "how do this collection's rows distribute across origin, activity, and age" meant re-deriving scorer behaviour by hand.
memory_stats — lifecycle + ranking-metadata aggregates
Deterministic SQL aggregates per collection: counts by active state, origin×active
cross-tabs (fs / api / legacy-NULL rows from the v0.34.0 ownership model), pinned
counts, accrual (7d/30d) and created-at span, deactivation reasons, and
mean/median/min/max distributions over access_count, confidence, quality_score,
and effective-time age (authored_at ?? modified_at — the same axis recency ranking
decays on since v0.27.0). Counts and cross-tabs cover all rows; distributions cover
ACTIVE rows only, since ranking never sees inactive rows. Complements index_stats
(embedding coverage / content types); nothing is filtered — system collections
included. Fail-loud by contract: a stats tool that silently dropped a section would
report a smaller vault as if it were the whole truth.
memory_rank — composite ranking explanation
Runs the real FTS + composite pipeline for a query and returns each result's
per-factor breakdown — weights applied (default | query profile, with the
recency-intent override behaving exactly as in production), recency and
blended-confidence inputs, quality/length/frequency/canonical multipliers, signed pin
delta, co-activation — plus raw-vs-composite rank shifts.
The details that matter:
The breakdown is captured inside the scorer, never recomputed. The recorded factors reproduce
compositeScoreexactly, andexplainchanges no score and no ordering — the diagnostic cannot drift from the thing it explains.Raw ordering is production's —
search's non-recency regime including its pin → legacy-composite → path tie contract — not a re-sort of the composite-ordered array, which would let exact raw ties inherit composite order and make the diagnostic circular.Output is the union of the composite top-N and the raw top-N. A raw winner the composite ordering demoted below the cutoff stays visible (
demotedRawWinner,⚠ demoted raw winnermarker) with its true composite rank — the exact signature a ranking investigation looks for, and the one a composite-only view would hide.The pin cap is reported truthfully. On composite surfaces the pin boost is
min(1.0, score + 0.3)— and quality (×1.3), frequency (×1.10), and canonical (×1.14) multipliers can push a pre-pin composite above 1.0, so the cap can CLAMP a pinned doc below its unpinned twin.memory_rankrenders that as a negativepinΔrather than hiding it. Known defect, queued behind a judged golden set; scoring behaviour is deliberately unchanged in this release.Ranks are keyed by filepath — docids are content-hash prefixes, so identical-content documents at different paths share a docid and would otherwise overwrite each other's rank.
Both tools return structured errors carrying the available list on unknown
collectionorvaultnames.memory_rankjoins the default-_clawmem-filtered tools (includeInternal: truereaches system memory);memory_statsfilters nothing.memory_rankcandidates are FTS-only — no vector or LLM stage; deterministic and cheap enough to run casually.No schema change, no migration, no reindex, no re-embed.
v0.35.0 — secondary-vault surfacing is now opt-in
Multi-vault deployments: the context-surfacing hook used to merge results from a
configured secondary vault into every prompt's injected context, unconditionally. Anything
indexed into that vault could surface in any session — vault separation held for explicit
queries but not for the automatic feed, and there was no switch.
Automatic cross-vault surfacing is now a config gate, default OFF: out of the box the
hook reads only the general vault, and a configured secondary vault stays isolated unless
deliberately queried (a vault-parameter MCP call) or deliberately opted back in:
# ~/.config/clawmem/config.yaml # or env, which wins:
retrieval: # CLAWMEM_SURFACE_SECONDARY_VAULTS=true
surface_secondary_vaults: trueThe details that matter:
The automatic secondary lane is the configured named
skillvault (the same one the recall mirror attributes usage to); the knob does not fan out across every named vault.The gate governs the automatic hook feed only. Explicit
vaultMCP calls,list_vaults,vault_sync, and Stop-hook usage attribution are unchanged.Opted in, behaviour is exactly the old one — including best-effort fail-open when the secondary vault is unavailable.
One gate, one producer: every downstream secondary-vault path in the hook keys off the tag the gated block sets, so OFF starves them all — including the recall mirror, which then writes nothing into the secondary vault.
Config is process-cached: restart long-lived processes (watcher, MCP server) after changing it; hooks are per-event processes and pick it up on their next run.
If secondary-vault content also reaches the general vault as an ordinary indexed collection, that is a separate route this gate does not touch — remove the collection from the general config if full isolation is the goal.
No schema migration, no reindex, no re-embed. Only the literal
true(env) / booleantrue(yaml) enables.
v0.34.0 — origin-aware reconciliation: DB-born memories survive filesystem indexing
The filesystem absence reconciler treated every active row in a collection as
filesystem-owned: any stored path missing from disk was deactivated absent on every pass.
Rows created directly in the database — hook observations, saveMemory output, beads sync,
REST writes — have no backing file BY DESIGN, so sharing a collection with indexed files
destroyed them wholesale (measured in one production vault: 2,430 of 2,437 DB-born rows
inactive). Every document row now records its lifecycle owner, and reconciliation only ever
touches rows the indexer actually owns.
The ownership model
A new documents.origin column: fs (created/maintained by the filesystem indexer;
reconciled against disk), api (DB-born; absence on disk means nothing), NULL (ambiguous
legacy row — exempt). Ownership is DECLARED by writers or ADOPTED on first touch, never
inferred: the indexer stamps fs on every path it takes — insert, update, reactivation, and
the unchanged short-circuit — while saveMemory and insertDocument default-stamp api.
There is deliberately no migration backfill: content_hash proves nothing about ownership
(mined imports write it through the same pipeline), so legacy rows heal only when a writer
touches them. On a pre-migration schema the reconciler enumerates nothing at all —
fail-closed, reported by an open-time warning — and any other enumeration error propagates
instead of widening the enumeration.
Ownership boundaries, both directions
A path collision across origins is rejected visibly, never resolved by silent takeover.
saveMemory preflights the requested path before ANY write: an active filesystem-owned path
throws, and an inactive row at the path throws too — lifecycle decisions are not overwritten,
per the v0.31.0 rules. Its write phase is transactional, so a mid-flight indexer claim rolls
the whole write back rather than leaking orphaned content, and dedup only counts against
API-claimable candidates through an ownership-conditional atomic update, so a concurrent
claim is never overwritten. Symmetrically, the indexer skips (and warns about) a file
appearing at an API-owned path — active or inactive — instead of adopting it.
clawmem mine is additive now
Mine imports run in a new importMode: rows are stamped api on every write path, and
absence reconciliation is skipped entirely — the staging snapshot is transient, so a later
mine into the same collection no longer deactivates earlier batches.
Upgrading
The origin column is added automatically on first open; no action is required. Two
behaviour changes to know about: a row whose file was already deleted BEFORE upgrading is no
longer auto-deactivated (nothing proves the indexer owned it — retire it with memory_forget
or a lifecycle sweep if unwanted), and a saveMemory write to a path occupied by a
filesystem-owned or inactive document now fails loudly instead of silently overwriting.
v0.33.0 — the causal writer returns, evidence-first
v0.32.0 rebuilt causal reading and noted that restoring causal inference was a separate,
future piece of work. This is that piece. A new causal witness writer runs inside the
decision-extractor Stop hook — off by default, shadow-first — and the causal graph it builds
is evidence-preserving end to end: documents are the nodes, and every edge carries the specific
fact pair that justified it.
The causal witness writer
When enabled (CLAWMEM_CAUSAL_WRITER=shadow|on), each Stop-hook invocation considers this
session's new observation documents against a small temporal window of recent ones
(CLAWMEM_CAUSAL_WINDOW, default 5, effective-time ordered) and makes ONE strict single-shot
model call proposing causal pairs. Admission is unforgiving: every proposed pair must cite fact
ordinals that exist in the persisted documents (caller arrays are never trusted), must include
at least one endpoint from this invocation (history is never re-inferred), and self-pairs,
duplicates, out-of-range ordinals, and sub-threshold confidences are each rejected with their own
audit event. What survives is written append-only as fact-pair witness sightings — repeat
observations of the same causal claim accumulate instead of being dropped — and the edge weight
is derived (MAX of witness confidences) in the same transaction. Placeholder reasoning is
rejected, so every edge a reader sees can show why it exists.
Every invocation in shadow/on mode writes a durable run record (causal_runs /
causal_run_events) — including early exits, skipped phases, and invalid configuration — with
retention mirroring the judge audit (90 days / 10k runs; sightings survive pruning). Inspect it
with the new clawmem causal-audit CLI. Shadow mode runs the full pipeline and audits everything
while writing zero graph state: run it for a while and read the audit before arming on.
Pre-cut edges: preflight, resolve, restore
Causal edges written by the pre-v0.30 writer carry old-format metadata. The reader synthesizes
witness evidence from the valid ones in memory for display (nothing is written on read); the
armed writer materializes that evidence durably the first time it touches such an edge; edges
whose metadata cannot yield a valid witness make the writer fail closed rather than guess. clawmem migrate causal-witnesses --preflight reports a
census and emits a binding manifest; --resolve-unmaterializable keep-weight|retire-edge acts
only on explicitly selected edges whose full row image still matches the manifest fingerprint;
retirement is archive-style and reversible (--restore-edge, fail-closed: a restore never
overwrites an occupied key). CLI audit rows are born terminal-pessimistic — a crash mid-operation
can leave an honest cli_error, never a stranded in_progress or a fabricated success.
The Stop hook now runs to a deadline
CLAWMEM_STOP_BUDGET_MS (default 25000) is a whole-handler deadline captured at entry. Every
model-bearing phase — observation extraction, contradiction candidate retrieval including its
embedding call, the contradiction judge, dedup embeddings, and the causal step — checks
remaining budget before starting and is skipped (audited, loudly logged) rather than started
unbounded. Previously a slow inference server could push the hook past the host's timeout, losing
the entire extraction; now the hook degrades phase by phase and always reaches persistence.
Security: document-id lookups are structurally validated
Found by this release's boundary tests: docid resolution used its input in a SQL LIKE pattern
without escaping, so the wildcards _ and % matched arbitrary documents through every
docid surface — including the destructive REST /documents/{docid}/forget, where a single _
deactivated whichever document the pattern happened to match first. Docids are now validated
against ^[0-9a-fA-F]{6,64}$ (after # strip) before any query; wildcards, non-hex, and
undersized prefixes return not-found everywhere. Oversized docids no longer throw. If you expose
the REST API beyond localhost, upgrade for this alone.
find_causal_links returns evidence, not just links
The tool (MCP + REST /causal-links) now returns directed edge records: invariant
source/target, traversal provenance (predecessor, depth, direction), and up to 3 fact-pair
witnesses per edge with reasoning, confidence, and sighting counts — projected in SQL, capped by
a byte ceiling (64 KiB) that drops whole edges from the tail rather than truncating mid-record.
Traversal is level-synchronous and returns the globally strongest 50 edges under a total order,
not the first 50 found. Depth > 1 remains per-edge evidence, not a verified chain.
Migration
Automatic on first open, additive only: four new tables (causal_runs, causal_run_events,
causal_witness_sightings, retired_causal_edges) with their indexes. No data backfill, no
manual step. The writer defaults to off, so nothing changes in write behavior until you arm it
— and before setting on, run the preflight (above).
Verification
Cross-model adversarial pass (codex / GPT-5.x), both gates in one pinned session: the design was reviewed to zero remaining findings across seven turns (13→9→9→8→3→2→0), then the implementation against that contract across seven more (13→5→4→6→3→1→0). Production-boundary tests drive the real CLI subprocesses (conflict, whole-run lock, rejected finalization), the real MCP handlers, and the real REST server (docid injection attempts, byte-ceiling overflow, destructive-route validation). Full suite at clearance: 1977 tests, 0 failures.
What didn't change
The v0.32.0 reader pipeline (shared causal retrieval, observation lane, one-hop traversal) is
untouched. includeInternal semantics are unchanged. With CLAWMEM_CAUSAL_WRITER=off (the
default) the Stop hook makes no causal model call and writes no causal rows — the only new
default-on behaviors are the budget deadline and the docid validation.
v0.32.0 — causal answers reach their reasoning
Three defects found while reviewing the causal layer's foundations: the recommended causal retrieval route could not reach the documents causal edges actually connect; the four causal surfaces had drifted into four different pipelines; and the SPO knowledge graph wrote evidence it never showed while silently dropping repeat sightings.
One causal pipeline behind every causal surface
memory_retrieve's causal mode, intent_search, query_plan's graph clauses, and REST
/retrieve's causal mode were four independent implementations of "intent-classified retrieval
plus graph expansion", each missing different stages — the documented claim that causal queries
route to intent_search's pipeline was true of none of them. They now share one implementation
and differ only in declared stages and visibility policy, so parity holds by construction. REST's
causal route gains graph traversal (it was anchor-only RRF), and the REST classifier now
recognizes the same causal phrasings as MCP — "why were" and "because we" never routed causal
over REST before. intent_search keeps its documented unfiltered contract verbatim, and its
enable_graph_traversal: false now disables every graph stage, including the new one-hop step.
WHY queries reach observation documents — and can walk a causal edge backward
The default-filtered routes excluded the _clawmem collection categorically, and every causal
edge endpoint lives there — so causal graph structure was reachable by nobody through the
recommended default route. Independently, neither traversal engine could follow a causal edge
backward (inbound edges were restricted to semantic/entity), so "why did B happen?" anchored at B
could never reach cause A.
WHY-classified queries on the filtered routes now anchor into _clawmem observation documents
specifically — never handoffs or deductions — and follow causal edges one bounded hop in both
directions (at most 3 per anchor, 10 total), with each hit labeled cause or effect relative
to its anchor. Candidate eligibility (active, non-invalidated, inside any effective-time window,
collection policy) is now enforced inside every anchor, traversal, and MPFP query — previously an
inactive or invalidated document could still consume beam slots and steer expansion even though it
was hidden from output. Entity co-occurrence expansion cannot honor row-level eligibility (its
aggregates carry no source-document provenance) and is therefore confined to direct
intent_search, its only shipped home, with the limitation documented.
The knowledge graph shows its evidence
entity_triples recorded source_doc_id and source_fact on first sighting, but no query
surfaced either — and a repeat sighting of a known fact was dropped entirely, so corroboration
accumulated nowhere. A new entity_triple_provenance table now accumulates unique evidence
sources per fact, written in the same transaction as the fact itself. kg_query reports
evidenceCount plus up to 5 most-recent sources per fact (evidence with no source document
renders as unattributed). Provenance in queryEntityTriples is opt-in, so the prompt-hook path
pays nothing new.
Migration (automatic, on first open): one evidence row is backfilled per existing triple that carries inline evidence. The backfill is idempotent and read-guarded — steady-state opens perform no writes.
Verification
Cross-model adversarial pass (codex / GPT-5.x): the remediation design was reviewed to zero remaining findings across five turns before implementation, and the implementation reviewed to zero remaining findings against it. Production-boundary tests drive the real MCP handlers and the real REST server: backward one-hop retrieval, decision-typed observation endpoints, lane exclusions for handoffs/deductions, breadth caps, the graph master switch, REST classifier parity, evidence dedup/ordering/atomicity, and migration idempotence across reopen.
What didn't change
The causal writer — batching, edge schema, per-edge witnesses — is untouched; restoring causal
inference itself is a separate, future piece of work. Direct intent_search remains unfiltered by
design; REST remains unfiltered in every mode; includeInternal semantics are unchanged;
confidence does not move on repeat sightings; hooks issue no new queries.
Upgrading
No manual migration step — the provenance backfill runs on first open, idempotent and read-guarded. The v0.31.0 mixed-version caution applies unchanged: when long-lived writers run concurrently, restart daemons and reconnect open agent sessions together, so no old-code process keeps writing after the vault has migrated. A stale writer inserts facts without evidence rows — repaired by a later open's backfill — and keeps dropping repeat sightings outright, which is not recoverable: the evidence row is simply never written.
v0.31.0 — forget stays forgotten
Three defects on the indexing boundary, all of which destroyed or reversed memory silently. Each was found by observing the database rather than by reading call sites, and each is fixed by making the affected write refuse rather than by adding a confirmation step.
memory_forget was undone by the next reindex
Forgetting a document that has a file behind it did not stick. The indexer reactivated any
inactive row it found at a path present on disk, without asking why that row was inactive — and
active is written by three unrelated owners: absence reconciliation, memory_forget, and
lifecycle archival. On a machine running clawmem watch, a forget therefore survived only until
the next .md change anywhere in that collection. Archived documents came back too, still carrying
archived_at, leaving them simultaneously active and archived.
Documents now record why they were deactivated (deactivated_reason), and only what was
deactivated for absence can be revived automatically. A forgotten or archived document stays that
way, and its file is skipped rather than re-inserted — re-inserting would have left the row
forgotten while making its content live again under a new id. The generic reactivation helper
enforces the same rule, so automatic profile regeneration during clawmem update no longer revives
a forgotten profile either. Restoring an archived document remains lifecycle_restore's job.
Migration (automatic, on first open): existing archived rows are backfilled as 'archive', and
any row a previous version left in the inconsistent active-and-archived state is restored to
archived, with a count reported on stderr. Use lifecycle_restore to bring any of those back.
Legacy rows deactivated by forget before this release are indistinguishable from absence and remain
reactivatable once; from this release forward, forget is durable.
clawmem reindex --force orphaned every database-created memory
--force began with a blanket UPDATE documents SET active = 0 across the entire vault, then
rebuilt only the configured collections. Observations, handoffs, and deductions live in
_clawmem and have no file behind them, so nothing ever brought them back. The deactivation set no
archived_at, which meant lifecycle_restore could not see them either — they were invisible to
the only supported restore path.
--force now does what its name implies: it re-reads and rewrites every file, bypassing the
content-hash short-circuit that normally skips unchanged documents. It no longer deactivates
anything up front; documents still absent from disk are deactivated by the ordinary reconciliation
pass, one collection at a time, exactly as they are without the flag. Separately, _clawmem is now refused by the indexer outright — database-created
memory has no filesystem source and cannot be reconciled against one, by any caller.
A failed enrichment blanked learned A-MEM notes
constructMemoryNote fails open to an empty note when the inference endpoint is unavailable, and
the note was then stored unconditionally. A transient outage during a routine reindex therefore
blanked amem_keywords, amem_tags, and amem_context on every document it touched. Writing an
empty note over a never-enriched row was quieter but no better: it made a failure
indistinguishable from completed enrichment, so backfill never retried that document.
An empty note is now never written, whatever the row holds — including a note whose entries are
only whitespace, which carries no information but would still have masked the row from backfill.
The store reports whether the note actually landed, and both callers honour that: the enrichment
log distinguishes a refused write from a completed one, and the consolidation backfill skips the
link pass and leaves the document eligible for a later retry. NULL is preserved as the retryable
"not enriched yet" state.
Upgrading
No manual migration step. The migration runs on open, is idempotent, and performs its count and repair in one transaction so concurrent opens cannot double-report. If it cannot run — a locked database, for instance — it says so on stderr and is retried on the next open rather than failing silently. If it reports repaired documents, those were archived rows a previous version had incorrectly reactivated.
One operational requirement when long-lived writers run concurrently — the watcher,
clawmem serve, or an MCP server in an agent session that stays open across the upgrade:
restart them together (daemons restarted; open sessions reconnected via /mcp or closed), so no
old-code process keeps writing after the first new-code open has migrated the vault. An old-code
writer records no deactivation reason, and the new indexer deliberately treats reason-less
deactivation as legacy absence — a forget issued through a stale session can be reactivated by
the next re-index, which is this release's bug reintroduced by the stale process. A
single-session setup with no daemons needs nothing.
v0.30.0 — ClawMem stops deleting rows
ClawMem's governing rule for agent-mediated memory mutation is that nothing an agent does should be unrecoverable. Every mutation surface honored that except one: purgeArchivedDocuments physically DELETEd rows. This release removes physical deletion from the package entirely rather than gating it, because a gate cannot work here — see below.
Why removal rather than an administrator gate
The obvious fix is to keep purge and require administrator authority for it. That fix does not hold, and the reasoning is worth stating because it generalizes: ClawMem's primary consumer is a coding agent with shell access. Any credential expressible in-process or on the command line — an env var, a confirmation flag, an interactive prompt — is equally available to that agent, which can type an opt-in env var and a --purge flag as readily as a human can. A gate like that only relabels the operation; it does not remove it from agent authority. (No such flag or variable exists in this release — that design was built, reviewed, and rejected for this reason.) A longer confirmation boundary also satisfies neither "immutable prior revision" nor "supported restore": once the row is gone there is nothing left to restore.
So the capability is not offered. Reclaiming disk space is an out-of-band operator action on the SQLite file, explicitly outside ClawMem's mutation contract. A supported retention design — reversible quarantine with a protected window — is planned separately.
What was removed (src/store.ts)
purgeArchivedDocuments— ranDELETE FROM documents WHERE active = 0 AND archived_at <= ?. Reachable from three call sites, below.deleteInactiveDocuments— ranDELETE FROM documents WHERE active = 0: strictly broader, destroying every inactive row (archived and forgotten) with no age bound and no authorization. It had no callers but sat on the publicStoreinterface.the store-level
removeCollection— ranDELETE FROM documents WHERE collection = ?, an unauthorized hard delete of an entire collection. Also callerless;clawmem collection removegoes throughcollections.ts(YAML config only), which remains the reversible path.
No code path now issues a DELETE against documents.
The three purge call sites, and the one that mattered most
lifecycle_sweep(MCP) — a non-dry-run sweep archived, then deleted every archived row pastpurge_after_days. That set was never previewed: the dry-run branch reportedWould archive N document(s)and said nothing about deletion, and the deleted population was different from the previewed candidates entirely. The preview was not weakly binding — it was silent.staleness-check(SessionStart hook) — the worst host of the three: unattended, no model and no operator in the loop, the purge count discarded, all of it inside acatch {}documented as "lifecycle errors never block the hook", so a failed delete was silent too. Auto-archival here is unchanged and remains reversible.clawmem lifecycle sweep(CLI) — archives only, and says so when it seespurge_after_daysset.
Count reporting was wrong (src/store.ts)
archiveDocuments and restoreArchivedDocuments returned SQLite's changes, which counts the documents_fts trigger writes as well — archiving 3 documents reported 16. Every "archived N" / "restored N" ClawMem has printed was inflated. Both now count the affected documents explicitly inside the same transaction. The mutations were always correct; only the reported numbers were wrong. Found by writing the first behavioral test of this path.
Behavior changes to expect
purge_after_daysis inert. It is still parsed so existing configs load, but only a positive finite value is accepted — a negative value previously produced a future cutoff that deleted every archived row, including ones archived moments earlier.lifecycle_sweepreportsarchived N document(s). Nothing was deleted.The tool description no longer advertises purge.Archive/restore counts are now accurate, and smaller than before.
Test coverage: tests/unit/no-hard-delete.test.ts — the invariant that no path destroys a row, the real SessionStart hook exercised end to end, and the count regression. There was previously no test of the purge path at all, which is part of why this survived.
v0.29.0 — the contradiction judge: opt-in model override, judge-gated analysis, durable audit
v0.28.0 repaired the contradiction write path; this release faces the model behind it. A live contract probe at the production seam showed the prescribed stock expansion model cannot meet the judge contract at all — it returns an object where the contract requires an array, echoes the schema's enum text back as a value, and fabricates confident relations between unrelated facts, deterministically. The same weak model sat in a fail-open position at the merge-time gate, where a parseable answer bypassed the deterministic heuristic and a missing confidence defaulted to exactly the actionable threshold. This release routes every contradiction decision through an explicitly configured judge, disables the analysis when none is configured, fixes the fail-open surfaces, and makes every evaluation durably auditable.
Judge-gated analysis — a behavior change (src/judge.ts, src/hooks/decision-extractor.ts)
No judge configured ⇒ contradiction analysis is DISABLED, as an audited no-op with one loud line per run — never a silent attempt on the stock model. This honors opt-in strictly: the reshaped prompt (below) could have made the stock model start clearing admission, activating erosion for users who never chose it.
Migration note: the evidence that "no verdict ever applied" holds for stock deployments. If you had pointed the global
CLAWMEM_LLM_URL/CLAWMEM_LLM_MODELat a larger or cloud model, real verdicts may have been applying — the judge no longer rides the global vars, so setCLAWMEM_JUDGE_*to keep that behavior. ClawMem deliberately never auto-adopts the global endpoint as a judge: the judge vars are also the data-egress consent (new decisions + retrieved snippets go to the judge you configure).clawmem doctorwarns when it can detect this situation; docs/guides/upgrading.md is the authoritative notice.Three lanes, one strict contract: any OpenAI-compatible endpoint (
CLAWMEM_JUDGE_URL+_MODEL+_API_KEY), the Anthropic Messages API (CLAWMEM_JUDGE_PROVIDER=anthropic, default modelclaude-haiku-4-5— the recommended default), or a sandboxed headlessclaude -pon a Claude Code subscription (claude-cli:--safe-mode, no tools, no MCP, no session persistence, untrusted payload over stdin only, recursion-guarded). The judge never runs local inference and never auto-downloads a model. The Haiku recommendation is backed by a shipped, passing capability-evaluation artifact;claude-sonnet-5is an unverified upgrade candidate — its preserved CLI-lane artifact shows zero fabrication with contradiction recall 3/4 and long-input 0/2 (borderline label + spawn-budget timeouts), and no API-lane artifact exists yet.
The merge-time gate had two fail-opens (src/merge-guards.ts, src/consolidation.ts)
The LLM layer bypassed the heuristic whenever its answer parsed, and a missing
confidencedefaulted to0.5— the exact actionable threshold. With a fabricating model that could block valid merges or, undersupersede, deactivate rows. The legacy object contract, permissive extraction, and confidence-defaulting are all removed: the gate now speaks the same strict relation-array contract as the hook judge, and a missing confidence is an invalid entry, never a default.The deterministic heuristic scores a disjoint-number pair at exactly the default threshold ("supports protocol version 1" vs "version 2" is actionable). Heuristic-only operation is therefore constrained to the non-deactivating
linkpolicy:supersederequires a configured judge. A configured-but-blockedsupersedeis loud — a runtime warning, amerge_supersede_blockedaudit event per occurrence, and aclawmem doctorreport that the policy is presently inactive. The effective policy is resolved once upstream and passed into the mutation helper as a parameter; mutation code no longer reads the policy env at all.Phase-2
linksets the old consolidated row'sinvalidated_bybacklink — it never inserted acontradictsedge; three docs that claimed otherwise are corrected (Phase 3 deductive synthesis is the edge writer).
Strict judge extraction (src/judge.ts)
The shared LLM-JSON extractor repairs a truncated array by keeping the complete elements — fine for enrichment, fail-open for a mutation consumer: token truncation silently applied a partial verdict batch. The judge path uses its own extractor: fence- and prose-tolerant, but a truncated JSON value is a typed reject, never a repair. Provider-reported truncation (
stop_reason/finish_reason) is rejected before extraction even runs.
Prompt reshape + injection fencing (src/judge.ts)
The old prompt's example row was invalid JSON (
"confidence": 0.0-1.0,"relation": "update|contradiction|same") — placeholder-echo bait for weak models, noise for strong ones. The reshaped prompt states the schema in prose, shows a valid example, and instructs that unrelated pairs — the common case — return exactly[].Instructions and data are role-separated. Vault-derived content travels JSON-encoded inside per-request CSPRNG-nonce markers and is declared data-under-analysis, never instructions. Fencing is a mitigation with tests (collision, marker imitation, nested instructions), not a proof — the strict admission pipeline, thresholds, bounded erosion, and the audit below remain the backstops.
Consumer-specific thresholds: the decision-erosion prompt states
0.7(its mutation threshold); merge prompts state your resolvedCLAWMEM_CONTRADICTION_MIN_CONFIDENCE— the prompt never overrides an operator setting.
Durable audit: judge_runs / judge_events (src/judge-audit.ts, src/store.ts)
Interactive hosts do not persist successful hook stderr, which made the previous release's shadow output unreachable exactly where calibration needed it. Every judge evaluation now writes durable rows: a run (consumer, lane, model, endpoint, prompt version, response hash, outcome, admission counts) plus per-verdict / per-reject / per-error events with scores, namespaced targets, and reason codes mirroring the real validator verdicts.
Mutation-authorizing evaluations commit audit and mutation in one transaction — an audit failure rolls the mutation back (Phase-3 deductive checks commit a precondition audit first; non-mutating outcomes write standalone best-effort rows). That is a deliberate fail-closed trade: an unauditable erosion is this feature's original defect, and audit coupling intentionally expands the set of errors that fail the feature closed — same-store transactions keep that trade cheap, not free. A judge failure that falls back to the deterministic heuristic records BOTH runs, linked, so neither the failed lane's identity nor the actually-deciding classifier is ever lost. Caller cancellation (
aborted) is terminal: audited, no heuristic, no mutation.Calibration is now audit-based on every host — including hosts that discard hook stderr. The previous guidance that shadow calibration was impossible under OpenClaw is reversed; query the audit rows instead.
clawmem doctor (src/clawmem.ts, bin/clawmem)
New judge check: a three-scenario smoke test against the configured judge — a designed contradiction must yield exactly one
(0,0,"contradiction")verdict at ≥ 0.7, an unrelated control must come back exactly empty (any verdict there is fabrication), and the merge single-pair contract must be actionable. An installation check, never capability certification. Unconfigured ⇒ the capability-floor note, the migration warning (provenance-aware — the wrapper marks its own stock default URL so stock installs never false-positive), and the inactive-supersedereport. Plus audit row counts.
Full suite 1,821/0 (92 new tests, including the truncation-repair regression, the audit-rollback fail-closed pin, the argv-content invariant for the subscription lane, pair-linkage retention, and a production-boundary integration suite driving the real Phase-2 gate with live judge lanes). The design went through a nine-turn cross-model adversarial DESIGN review (codex / GPT-5.x) to verbatim "Zero remaining findings." before implementation — thirty-three findings absorbed across the turns, including the merge-gate fail-open, the truncation-repair fail-open, and the accidental-activation hazard that forced the symmetric disable — and the implementation went through further adversarial CODE review rounds whose findings (among them: a failed-over heuristic could authorize supersede; a dirty response wrapping one valid verdict could decide) are all folded and regression-pinned.
v0.28.0 — hook write-path contracts: extraction guards, honest counters
Two Stop-hook write paths — A-MEM causal inference and contradiction detection — reported success while persisting nothing. Instrumentation counted attempts against INSERT OR IGNORE, so a total write failure was indistinguishable from a healthy run. This release makes the reporting truthful, hardens the validation ahead of every mutation, and repairs the two contract defects that left contradiction detection unable to apply a classification at all. The terminal mutation that repair unblocks — invalidation — ships shadowed behind an opt-in flag, not armed.
Counters report outcomes, not attempts (src/amem.ts, src/store.ts, src/mcp.ts, src/server.ts)
inferCausalLinkssums.changesfrom each insert instead of incrementing per candidate, and warns when candidates clear every filter but no row lands (the signature of silentINSERT OR IGNOREsuppression). The summary line now reports proposed / passed-filters / placeholder-rejected / range-rejected rather than a single fabricated success count.Both graph builders (
buildTemporalBackbone,buildSemanticGraph) count inserted rows the same way — the two feed one response and previously reported in different units.build_graphsreports standing totals alongside the delta, on the MCP tool and the REST endpoint:N new edge(s), M total. An idempotent rebuild legitimately writes 0 new edges, which read as "the graph is empty" without the total. Response-shape change — see below.Totals count the ACTIVE graph. Only edges whose both endpoints are active are counted, matching the population the builders operate on; a raw count reported edges the live graph no longer contained. Shared via
store.countActiveRelations()so the two surfaces cannot drift.
Validation ahead of every mutation (src/hooks/decision-extractor.ts, src/schema-placeholder.ts)
validateContradictionEntryis a pure, exported, typed-verdict validator applied to every classifier entry before any document is mutated: exact relation-enum membership, stricttypeof reasoning === "string"(coercion was a fail-open on every JSON-valid non-string), finite[0,1]confidence, and both index bounds.Array-level admission (
admitContradictionEntries, exported and pure). Only repeats identical across the fields that can drive a mutation (relation, confidence, reasoning) are collapsed; a pair the classifier answered more than one way — differing label, confidence, or reasoning — is dropped whole. Repeats compounded the confidence penalty on a single document and could cross the invalidation floor a single classification never reaches, and a first-wins collapse would have made the outcome depend on array order. Whether several distinct new facts may penalize one old document repeatedly is a separate open question, unchanged here.The shared anti-parrot guard normalizes before matching. Residue comparison folds NFKC, drops
Default_Ignorable_Code_Pointcharacters, lowercases, collapses whitespace runs, and strips outer punctuation — so a doubled space, a newline, fullwidth text, a trailing period, or an invisible zero-width character inside a word no longer walks past the guard. Internal punctuation is deliberately not normalized: doing so mapped plausible identifiers likecanonical_entity_nameonto blocklist entries. Marker detection folds NFKC, drops invisibles, removes every template marker, and asks whether any letter or digit remains — so arbitrary punctuation envelopes (**{{x}}**,- {{x}},|{{x}}|,【{{x}}】, fullwidth{{x}}) and multi-line markers are all caught without enumerating wrapper characters. A value whose every letter and digit sits inside a marker is filtered as carrying no assertable content, whether it is echoed residue or a bare code fragment; content that merely contains a marker, like"${HOME} is the user home directory", is untouched. That applies to claim fields only. Identifier fields — conversation-synthesis aliases and link targets, SPO subjects and objects — are names, not assertions, so no marker shape is rejected there on shape alone (${HOME}is a legitimate object ofuses;{{user.name}}is a Handlebars path). Their residue is caught by consumer-scoped sets naming the exact skeleton that field's own prompt emits. Link targets were previously unguarded entirely despite their prompt carrying a copyable skeleton. Residue sets stay consumer-scoped — a memory about this defect is legitimate content, not residue.The contradiction parse gate reports instead of returning silently, emitting response shape, length, content hash and served model identity. Raw model text stays opt-in behind
CLAWMEM_DEBUG_LLM_RAW— the prompt carries transcript-derived material, so logging it by default would be a content-exposure path./no_thinkis applied idempotently — six prompts already carry the token inline for the local fallback and were being sent a doubled control token.
Contradiction detection could never apply a classification (src/hooks/decision-extractor.ts)
Two independent defects sat between a valid classification and any effect on the vault. Both are model-independent, and both failed silently.
The document lookup was handed a URI where it expected a path.
SearchResult.filepathis a virtual path —clawmem://<collection>/<path>, assembled in the store's projection — whilefindActiveDocumentmatches the baredocuments.pathcolumn. The hook passed the URI straight through, so no candidate could ever resolve to a row, and the miss exited through a barecontinue. Every classification that survived the parse gate and every validation above it died there without mutating anything. The path is now resolved withparseVirtualPathbefore lookup, and a target that still fails to resolve increments a counter and warns rather than disappearing.The parse gate rejected the deployed model's response shape. The model wraps its array in an object (
{"result": [...]}).parseLinkGenerationFromLLMinamem.tshas unwrapped that form since it was written; this path never did, so structurally valid responses were counted as malformed. Both forms are accepted now.
Taken together these are why contradiction detection had no observable effect on any vault regardless of what its logs reported. Precisely: the hook ran, called the model, and parsed a verdict — but no verdict could ever be applied, and no document was mutated, in any version shipping this implementation. Read it as newly-effective, not as newly-fixed.
Invalidation ships shadowed — CLAWMEM_CONTRADICTION_INVALIDATE
Repairing the lookup made the soft-invalidation branch reachable for the first time, so it is gated rather than simply switched on. The two mutations behind a contradiction are not symmetrical:
Confidence erosion (
-0.25, floored at0.2) is a ranking signal — 25% of the default composite. A degraded document ranks lower and stays fully retrievable. Live.Invalidation sets
invalidated_at, which is a hard predicate on the FTS join and both vector joins. An invalidated document leaves retrieval entirely, with no query-time signal that anything was suppressed. Off unlessCLAWMEM_CONTRADICTION_INVALIDATE=true.
Unarmed, the hook logs WOULD invalidate "<collection>/<path>" per suppressed write plus per-session summaries, so precision can be adjudicated against real traffic before anything is removed.
Shadow mode reports exactly what arming would remove — no more. Candidates are selected by pathname (decisions/, observations/), but the armed writer only touches content_type='observation': on the reference vault, roughly three times as many candidates as eligible rows. Eligibility is now asked once and gates the shadow log and the armed write identically, so calibration cannot be aimed at a population two thirds of which could never have been invalidated. Documents that reach the floor while ineligible are reported separately, and the armed write checks .changes rather than discarding its result — a swallowed outcome is how the original defect stayed invisible.
Exposure is vault-specific along five axes: the eligible population's confidence distribution ((0.95, 1.0] needs four classifications to reach the floor, (0.70, 0.95] three, (0.45, 0.70] two, <= 0.45 one), content-type mix, the classifier model, corpus semantics, and classification opportunity (only the top 5 search results per session are ever classified, from a pool that changes when the vector leg falls back to FTS). The contradiction invalidation guide has the measurement queries, the adjudication procedure, and the restore statements.
Eligibility is content_type='observation' only. A superseded decision is eroded but never retired — decision records are outside the writer's reach by design. Shadow output is not reachable under OpenClaw, where the plugin surfaces hook stderr only on a non-zero exit and a successful shadow run exits zero; this flag should not be armed on that host, and calibrating under Claude Code is not an equivalent substitute because exposure is host-dependent. No shipped systemd unit runs this hook — clawmem-watcher.service is the indexer.
Upgrade note — build_graphs response shape
Additive, but a contract change. Both surfaces gain temporalTotal / semanticTotal, and the MCP text line changes from Temporal graph: N edges to Temporal graph: N new edge(s), M total. Anything parsing that text should be updated. The REST endpoint continues to emit all four keys unconditionally (its pre-existing shape); the MCP tool continues to include only the graph types requested.
Quality gates
Full suite 1,775/0. Cross-model adversarial passes (codex / GPT-5.6) throughout — seventeen turns, including successive fail-opens in the same validator, a duplicate-entry defect that compounded mutations on one document, and a round in which the newly-added regression tests were shown not to exercise the guard they were written for.
v0.27.0 — authorship time: memories rank by when they were written
Fixes a confirmed recency-contamination defect: ClawMem had exactly one time axis per document (filing/update time), so a 2025 conversation mined today ranked — and filtered, and injected — as if written today. Worst under recency intent, where recency carries 70% of the composite weight. Documents now carry a second, nullable axis: authored_at, when the content was originally written.
Authorship capture end-to-end (src/normalize.ts, src/clawmem.ts, src/indexer.ts, src/store.ts)
Mining extracts message timestamps from all supported formats through strict per-format adapters — RFC3339-with-explicit-offset for Claude Code / codex / Claude.ai (timezone-less values are rejected, never host-interpreted; impossible dates are rejected, not normalized), epoch-seconds for ChatGPT / Slack with range guards. Each exchange chunk is stamped with the max timestamp within that exchange only — never a transcript-level fallback (privacy exports flatten multiple conversations into one stream, so a transcript max would cross conversation boundaries).
Synthesized facts inherit their source conversation's authorship; the shared
saveMemoryAPI advancesauthored_atmonotonically (a newer repeated assertion moves it forward; reprocessing older evidence never regresses it; NULL-safe).Any vault file can declare
authored_at:in frontmatter — full timestamp or date-onlyYYYY-MM-DD(UTC midnight), quoted or unquoted. Frontmatter is authoritative: removing the line clears the stored date on the next content change.Re-mining an existing vault dates documents without churn: a body-identical re-index whose only change is
authored_attakes a metadata-only "dated" transition —modified_atpreserved, stored confidence untouched, no re-enrichment queued.clawmem mine <dir> -c <collection> --backfill-dates [--apply]: recoverable-only backfill for already-mined vaults. Dry-run report by default; the apply phase is transactional and re-asserts the validated content hash on every row, so a concurrent change can never receive a mismatched date; documents whose content no longer matches the source are skipped, never guessed.Colliding transcript names no longer overwrite each other: sources that sanitize to the same staging name (
a/b.jsonlvsa_b.jsonl) previously clobbered each other silently; they now mine under distinct hash-suffixed identities, decided once per source and stable across re-runs (non-colliding sources keep their existing names — zero path churn).
Effective time in ranking and retrieval (src/memory.ts, src/mcp.ts, callers)
Composite recency ages documents by
authored_at ?? modified_at, and the confidence signal's internal recency uses the same effective date (the access-pattern sentinel stays on filing time). Documents without authorship behave exactly as before.Temporal filters and the temporal-proximity channel ("what did we plan in March") select, order, and score by effective time on both the FTS and vector legs.
Recent-content windows — postcompact recent decisions/antipatterns, session-bootstrap current focus,
clawmem reflect, directory context, profile — apply their cutoffs and display their dates on effective time, so a freshly-mined historical decision no longer masquerades as this week's work. Operational clocks (the dedup window, lifecycle sweeps, staleness review, session log) intentionally keep filing/update time.Result metadata: compact search results now carry
authored_at(null = unknown).
Entity-edge IDF population fix (src/entity.ts)
Completes the v0.25.0 hub-bias fix: the enrichment/edge-creation path computed IDF with an active-only numerator over an all-documents denominator, letting archived history deflate specificity (below zero in the extreme) and suppress edges for entities that are specific among the live corpus — and could create edges toward archived documents. Both populations are now active-only and archived candidates are excluded, matching the neighbor path. Bug-first tests demonstrate the pre-fix failure.
Quality gates
Full suite 1,681/0 (62 new tests, including direct caller-level pins for every recency window and a live CLI backfill lane). The authorship design was adversarially design-reviewed before any code (6 turns, 29 findings absorbed — including the two-axes consumer classification, the confidence-lane uniformity rule, and set-independent mine identity), then the implementation was reviewed to verbatim "Zero remaining findings — ship as is." in 3 turns (7 findings, among them a year-0000–0099 date-construction bug and a backfill validation race); the entity fix cleared in 1 turn. Cross-model adversarial passes (codex / GPT-5.6) throughout.
v0.26.0 — offline eval harness (evidence-overlap replay) + short memory-query gate fix
Retrieval quality becomes measurable: a gold-labeled replay harness scores the real query pipeline against hand-labeled evidence, ending the era of ranking/extraction changes shipping un-measured. Plus a gate-ordering bug fix that was dropping short explicit memory questions.
Offline eval harness (src/eval/, clawmem eval run)
An offline, CLI-only evaluation subsystem (pattern extracted from the HORMA paper's evidence-grounded reward, re-authored for ClawMem's deterministic on-device substrate):
Replays the real pipeline, never a mirror.
clawmem eval run --gold <file.jsonl>drives the actual registeredqueryMCP tool handler over an in-memory transport — expansion, RRF fusion, rerank blending, composite scoring, and MMR diversity at their tool defaults — so the number measures the product, not a copy that drifts.Doc-level evidence-overlap metrics: Jaccard
|C∩E|/|C∪E|between retrieved and gold document sets, plus precision@k, recall@k, hit@k, and MRR; per-tag slices; p95 latency. Artifacts:run.json(machine) +report.md(hand-audit companion).Gold sets are strict, hand-labeled JSONL (any path via
--gold, so private labels can live outside the repo): unknown fields, malformed lines, and duplicate ids are hard errors; an example with any evidence ref that doesn't resolve to an active document is excluded from scoring — regardless of its replay mode — and fails the run's trust gate, so partial or stale gold can never inflate recall.Trust gate, machine-visible: enough scored examples (
--min-examples, default 30), zero unresolved refs, and an explicit--auditedattestation that a 10–20% hand-audit of the labels passed. A completed run with a failing gate exits1(artifacts still written) so automation can't mistake an untrusted number for a trusted one.Identity integrity: retrieved results map back to document ids by inverting the
collection/pathdisplay path; zero or multiple matches (a collection name containing/colliding with a sibling) hard-fail the run instead of guessing or silently dropping — either would corrupt the metric.State-safe: the replay writes no retrieval, lifecycle, or telemetry state (
context_usage/recall_events/memory_relationsuntouched, regression-pinned); normal inference caches may populate as in any live query.--db <snapshot>points the whole run at aVACUUM INTOcopy for corpus-frozen A/B comparisons between checkouts.First build ships the
queryreplay profile;intent/contextreplay, benchmark adapters, and provenance are follow-on phases. Guide: docs/guides/eval-harness.md.
Short memory-intent queries now reach retrieval (src/hooks/context-surfacing.ts, src/retrieval-gate.ts)
The retrieval gate's FORCE_RETRIEVE_PATTERNS (memory verbs, temporal refs, personal-data queries) carry the explicit contract "(checked before skip)" — but the context-surfacing hook returned on prompt.length < 20 before the gate ever ran, so short explicit memory questions ("what did I say?", 15 chars) got an empty <vault-context> in violation of the gate's own contract. The force check is now consulted before the length early-return (new hasForceRetrieveIntent export, shared with shouldSkipRetrieval so there is one source of truth). Empty prompts, short non-memory prompts, slash commands, heartbeat suppression, duplicate dedupe, and the query-text privacy split (pre-retrieval skips never persist prompt text) are all unchanged.
Quality gates
Full suite 1,619/0 (42 new tests). Each item independently reviewed by a cross-model adversarial pass (codex / GPT-5.6) to verbatim "Zero remaining findings — ship as is.": the eval harness in 3 turns, the gate-ordering fix in 1.
What didn't change
No schema changes, no migrations, no re-embed. All runtime retrieval surfaces (hooks beyond the gate ordering, query, intent_search, search, vsearch, memory_retrieve, REST) score and rank exactly as in v0.25.0. No MCP tool was added — the eval harness is CLI-only.
v0.25.0 — extraction retry-with-error-feedback + decision half-life + entity-neighbor hub-bias fix
Three independently reviewed items from the strategic queue (§13.1, §36.11, BL-001) — the first queue burndown since the ranking was locked. Three files, three pipelines, no shared invariants.
§13.1 — retry-with-error-feedback on every LLM extraction path (src/llm-retry.ts)
ClawMem's extraction surfaces were single-shot: one generate() attempt, and any malformed/empty response failed open to []/null with no signal to the model about what went wrong — invisible data loss on every transient formatting miss. All eight extraction call sites now ride withRetryAndFeedback (pattern re-authored from Volt's llm-map validation loop):
Stateless corrective retries (default 3 attempts): each retry is a fresh
generate()call with a reconstructed prompt — original prompt + the parse error + a 500-char excerpt of the previous response — never a conversation continuation.Hard wall-clock deadline shared across all attempts: no attempt starts past the deadline, and an in-flight
generate()is raced against it — a backend that ignores the abort signal cannot hold the helper past the budget.Fail-open on exhaustion (null → the same
[]/nullcallers already handled), now with a terminal[llm-retry] <label>: exhausted…warning naming the call site.Call sites: observer
extractObservations/extractSummary, conversation-synthesisextractFactsFromConversation, A-MEMconstructMemoryNote/generateMemoryLinks/evolveMemories/inferCausalLinks, entityextractEntities. Parse closures own whole-response STRUCTURAL validation (so a malformed payload triggers a corrective retry instead of a silent post-loop drop — including integer-validated causal-link indexes, closing a partial-write path); semantic/domain filtering stays outside the loop.Accounting change: a transient failure that recovers on retry no longer counts as an LLM failure in
mine --synthesizestats; only terminal exhaustion does.
§36.11 — decision ranking half-life: ∞ → 180 days (src/memory.ts)
HALF_LIVES.decision = Infinity pinned recency at 1.0 forever, so a silently-abandoned decision ("we'll use X" → quietly moved to Y, with no contradictory write to trigger supersession) kept winning ranking indefinitely. Decisions now decay on a 180-day half-life. Ranking durability only: decision keeps its attention-decay exemption, nothing is deleted or archived (lifecycle policy is separate), the access-frequency extension still stretches frequently-resurfaced decisions toward 3×, and deductive/preference/hub/antipattern stay infinite. An unaccessed 180-day-old decision drops to recency 0.5 — still fully searchable, just no longer permanently ahead of fresher material.
BL-001 — getEntityGraphNeighbors hub-bias fix (src/entity.ts)
The entity-neighbor path ranked by raw co-occurrence count — reintroducing exactly the hub bias the edge-creation path's IDF suppression exists to prevent (one path suppressed hubs, the other reinforced them). Neighbor ranking now blends count with IDF specificity (min(1, log1p(count)/5) × clamp(idf/3.0), sharing the edge path's 3.0 threshold via ENTITY_IDF_SPECIFICITY_THRESHOLD):
Score-before-limit: every co-occurring candidate is scored, THEN the pool is capped at 30 — a specific neighbor at raw-count rank 31+ can now surface (the old SQL
ORDER BY count DESC LIMIT 30excluded it before scoring).Best-path-per-doc: candidates are traversed in blended-score order, so a document reachable via both a hub and a specific entity keeps the specific path's score and
viaEntity.Active-only IDF + hydration: IDF populations are active-documents-only (archived mentions can no longer distort specificity), archived-only candidates are dropped from the pool, and hydration excludes archived documents before its per-entity LIMIT. One grouped CTE query supplies counts and active doc-frequency together (no N+1).
Quality gates
Full suite 1,577/0. Each item independently reviewed by a cross-model adversarial pass (codex / GPT-5.6) to verbatim "Zero remaining findings — ship as is.": §13.1 in 3 turns, §36.11 in 2, BL-001 in 3 (bug-first — the failing hub-bias test predates the fix).
v0.24.0 — raw-BM25-primary ranking for search (judged keyword eval) + bypass A/B toolkit
v0.23.0 made the FTS relevance signal real and deliberately deferred any ranking-contract change to a judged eval. That eval ran: 43 judged keyword targets (23 discovery + 20 family-disjoint held-out) with objectively-labeled fairness shapes (14 raw-favorable "exact-old", 22 composite-favorable "fresh-among-many"), frozen floors and decision rules pre-registered before any comparison was computed. Raw-BM25-primary beat the shipping composite decisively: combined MRR 0.848 vs 0.415, hit@1 33 vs 6; held-out 0.875/16-at-#1 vs 0.335/zero-at-#1 (the composite missed the absolute floors outright); composite lost even on its OWN favorable shape (fresh-among-many 0.348 vs 0.801) — the recency/quality/co-activation multipliers bury keyword relevance rather than refine it. One paired regression (one position) in 43 cases; controls clean in both arms.
Behavior changes
searchranks non-recency queries by the RAW BM25 transform (|bm25|/(1+|bm25|)), mirroring the v0.22.0 vector-route pattern. Metadata — including pin — participates only inside groups of exactly-equal raw scores (deterministic tie order: pinned, then legacy composite, then path).structuredContentcarriesscoreBasis: "fts-bm25". FTS-transform values and vector cosines remain independent, non-comparable channels.minScoreonsearchnow filters the raw score for non-recency queries and has NO default — omitted means no filter, an explicit0is honored. Recency-intent queries ("latest…", "recent…") keep the composite regime with the previous default-0 floor and reportscoreBasis: "composite".Unchanged surfaces: hooks/context-surfacing,
query,query_plan,intent_search,memory_retrieve's composite modes, the CLIsearchcommand, and the REST API keep their existing scoring. The change is scoped to the MCPsearchtool, exactly as evaluated.Bypass ops escape hatch:
CLAWMEM_DISABLE_FTS_BYPASS=trueforces the full expansion path at both strong-signal-bypass consumers (MCPquerypipeline + CLIquery) — built for the 49.3 A/B harness, kept as an operational kill switch.expandQueryCacheKeyexported — the exactllm_cachekeyexpandQueryreads/writes, so eval harnesses can delete/verify expansion cache rows without replicating private key construction.Bypass characterization (49.3, frozen census): on a frozen 127-query census (51 judged keyword cases + 76 firing-hunt probes) over the frozen production snapshot, the strong-signal bypass fired on THREE — 3/51 on the judged set, 0/76 among the probes — all lone-or-near-lone pools, all with the correct top document. Frozen-corpus characterization only: the probes were selected to hunt firings, so these rates say nothing about production firing prevalence (unmeasured), and the firing set is snapshot-relative — one probe term began firing on the live index hours later as new documents mentioned it. On this frozen snapshot/census the gap ≥ 0.15 condition fired rarely — sibling documents suppress the gap on this corpus. Eval tooling:
scripts/eval-keyword-acceptance.ts(freeze/run, FTS-only) andscripts/eval-bypass-ab.ts(freeze/run, live-service A/B with a verified expansion-cache freeze and census integrity re-execution).Bypass A/B verdict (49.3): on the frozen census's complete fired population (3 natural cases; pre-registered zero-allowance gates; run gated on frozen service identity + an embed-geometry canary drift-check + rerank health), the bypass lost nothing — zero dropouts, zero hard regressions, bypass-arm MRR +0.038 higher — and saves ~56% wall time where it fires (1.9 s vs 4.3 s with a warm expansion cache). Verdict:
SAFE ON FROZEN CENSUS (n=3 fired; zero-allowance gates); population risk unvalidated— thresholds 0.85/0.15 stand on this census; no tuning proposed.
v0.23.0 — monotonic BM25 exposed score (the FTS relevance signal was a constant)
The v0.22.0 design gate discovered that searchFTS's exposed score was computed as 1 / (1 + Math.max(0, bm25)) — but FTS5's bm25() is negative-is-better and ≤ 0 for every match (0/4,962 positive rows measured on a production vault), so every FTS result carried the identical score 1.0. SQL ordering was correct; everything downstream of the exposed score was not: composite ranking on FTS surfaces (search, REST keyword, CLI, memory_retrieve keyword and its semantic-mode FTS fallback, hook FTS lanes) was effectively metadata-only, the query pipeline's strong-signal bypass could never fire on multi-hit queries yet always fired on single-hit ones, hook injection systematically preferred FTS-sourced docs over vector-sourced ones (1.0 vs cosine), and every score-threshold gate was vacuous.
Behavior changes
Exposed FTS score is now
|bm25|/(1+|bm25|)(ftsScoreFromBm25, exported): monotonic in match strength, bounded [0,1), per-row stable, clamps a hypothetical positive input to 0.searchkeeps the composite regime (a regime change is gated on the 49.2 judged keyword eval) — but its searchScore input is now real, so keyword relevance finally contributes ordering. Observedscore/compositeScorevalues shift accordingly;minScoresemantics are unchanged. Compact results report the composite; non-compact carry bothscore(raw transform) andcompositeScore.Strong-signal bypass is functional: fires only on a strong (≥ 0.85 ⇔ |bm25| ≥ 5.67), clearly separated (gap ≥ 0.15) top hit; a lone weak match no longer triggers it. One shared helper (
hasStrongFtsSignal) now backs both the MCPquerypipeline and the CLIquerycommand (previously a drifted duplicate).memory_forgettargeting is stricter and safer: the confidence gate (score ≥ 0.7, or a ≥ 0.2 gap when 2+ candidates exist) is live — previously every FTS candidate scored 1.0 and was auto-selected, including a lone garbage match. Weak matches now return the candidate list for disambiguation. Non-destructive pin/snooze behavior is unchanged.query_plan's graph clause now carries RRF-fused scores into graph traversal (parity with the causal andintent_searchpaths, via a sharedattachRrfScoreshelper) — traversal seed mass was previously anchored on raw single-channel scores.Consolidation dup-gate and curator BM25 probe are live: the
score ≥ 0.7duplicate filter and the> 0.3retrieval probe actually discriminate now. A near-empty vault may honestly report a degraded BM25 probe where it previously passed vacuously.Scale honesty: FTS-transform scores and vector cosines are independent monotonic signals, not a calibrated common scale. Mixed-channel merge points (REST hybrid max-merge, hook dedup) are no longer degenerate, but cross-channel calibration remains future, eval-gated work.
Follow-on work: BACKLOG 49.2 (judged keyword eval → search regime recommendation; reports bypass firings) and 49.3 (query-pipeline A/B before any bypass-threshold tuning).
v0.22.0 — raw-similarity-primary ranking for the direct vector routes
v0.21.0 removed the system-internal junk from the direct tools' results; the direct-pipeline eval it mandated then showed the composite scoring layer itself was the remaining defect on those routes: on a judged set against the live vault, pure raw cosine ranked 16/19 targets #1 (MRR 0.912) while the shipping composite ranked 1/19 (MRR 0.307), filtered 14/19 correct answers below the old minScore floor, and got WORSE with deeper candidate pools. Attribution was measured per stage: length normalization caused the floor kills; the pin +0.3 additive made one pinned, heavily-accessed hub document top-1 for nearly every query including nonsense controls; re-mixing the weights could not help because every multiplier is larger than the 0.03–0.10 raw margins that separate right answers from wrong ones in the compressed-high band of modern embedding models.
Behavior change: raw ordering on the evidenced vector routes
vsearchandmemory_retrievesemantic/discovery modes rank non-recency queries by RAW vector cosine. Document metadata — including pin — participates only inside groups of exactly-equal raw scores (deterministic tie order: pinned, then legacy composite, then path).structuredContentcarriesscoreBasis: "vector-cosine"; raw cosine is embedding-model-specific and not comparable to composite values.minScoreonvsearchnow filters the raw score for non-recency queries and has NO default — omitted means no filter, an explicit0is honored (nullish handling). Recency-intent queries keep the composite regime with its 0.3 default floor and reportscoreBasis: "composite".Recency-intent queries are unchanged everywhere (RECENCY_WEIGHTS composite, newest-first behavior, contentType priority sort), selected through one centralized regime function. The semantic/discovery FTS fallback (vector leg unavailable) also keeps composite — its scores are not cosine.
Unchanged routes:
search(BM25),query,query_plan,intent_search, hooks/context-surfacing, andmemory_retrievekeyword/hybrid/causal/complex.find_similarwas already raw-ranked and is untouched (docs now say so). A BM25 ranking eval is backlogged separately — its exposed score is currently non-monotonic (Math.max(0, bm25)flattens FTS5's negative-is-better scores), a pre-existing issue this release documents but does not change.Pin re-documented: pin = lifecycle retention + prioritization among relevance-equivalent results. On composite surfaces it keeps the +0.3 boost; on the raw routes it breaks exact ties only. "Persistent surfacing" — a pinned document floating above more relevant ones on every query — was the measured hub defect, not a feature.
retrieval.mcp_direct_tuned_weightsis superseded and has no effect. Its own gate evidence (the direct-pipeline eval) measured tuned weights at 1/19 hit@1. The key is still parsed; setting it logs a once-per-process warning.
Verification
bun test → 1504 pass / 0 fail (new: constructed-tie units — pin wins inside an exact-score tie group and never crosses a boundary; regime selection; no-default-floor + explicit-zero minScore handling; route-level raw-ordering regressions including the incident fixture, which now proves the inversion cannot recur even with includeInternal: true). A frozen, deterministic acceptance gate (scripts/eval-acceptance.ts: vault snapshot + frozen asOf clock + per-case frozen query vectors through the guarded precomputed-vector path) passed all predeclared criteria: pin-invariance rank identity on non-recency cases; exact match to the frozen composite baseline on recency cases; no control-query hub dominance and no pinned top-1; discovery set 23/23 in-pool, hit@1 18/23, MRR 0.870; held-out family-disjoint set hit@1 17/20, hit@5 19/20, MRR 0.879 against floors of 12/20, 17/20, and 0.75 declared before the set was authored. Design and implementation adversarially reviewed cross-model to explicit clearance (5-turn DESIGN gate, 14 findings folded).
v0.21.0 — vsearch trust hardening: internal-collection exclusion, geometry canary, embed survivability
A live incident exposed a stacked failure: an embedding server silently producing non-discriminating vectors for the vault's dominant register (a last-token model whose GGUF conversion lost its EOS-append flag), amplified by composite scoring floating system-internal docs over true matches — while every existing health check passed. The server-side cause is an operator fix; this release closes the client-side amplification and the detection gaps, and hardens the embed run that the remediation itself crashed.
Behavior change: _clawmem excluded from MCP retrieval by default
search, vsearch, query, query_plan, memory_retrieve, and find_similar no longer return the system-internal _clawmem collection (observations/deductions/handoffs) unless asked: pass includeInternal: true, or name _clawmem in an explicit collection filter. find_similar auto-includes internal neighbors when the reference document is itself internal. intent_search / find_causal_links / kg_query / session_log / timeline are unfiltered by design — system memory is their substrate. Exclusion happens at the store layer (SQL predicate for BM25; escalating MATCH depth for vectors) and inside graph traversal (excluded nodes are pruned before beam selection and score normalization), so internal docs neither appear NOR consume candidate/beam budget.
Vector-side contract: under exclusion the scan escalates depth until limit allowed documents hydrate, capped at 4,096 fragments. Cap-limited under-fills carry an explicit degraded: true + degradedReason (excluded-dominant when distinct excluded docs account for the shortfall, cap-truncation when fragment dedup drives it); multi-leg routes aggregate any(leg) with per-leg reasons in structuredContent.degradedLegs. Plain small-vault exhaustion returns a normal short list with no marker.
Embedding-geometry canary (preflight + doctor)
clawmem embednow runs a pair-separation probe battery BEFORE any destructive step — a broken-geometry server aborts the run before--forceclears anything (override:--force-geometry). The battery uses the production embed templates and includes terminus + truncation controls that catch unanchored last-token readouts; self-similarity alone cannot (stored-vs-fresh stayed 0.999 through the entire incident).Baselines are stored per (probe-version, model, dimension) profile in a new
embed_canarytable as first-healthy calibrations — healthy runs never roll the reference;clawmem embed --force --recalibrate-canaryis the explicit replacement operation (intrinsic sanity floors govern its gate, since the old baseline is exactly what it replaces). Margins alert relative to the calibrated baseline (< 50%) with an absolute backstop, and drift (stored-vs-fresh probe cos < 0.98) flags a changed serving stack behind an unchanged model name. Mixed dimensions/models across one battery (a flapping endpoint) are a hard failure, and a--forceclear never proceeds on an UNVALIDATED endpoint. Mid-run drift, an unverifiable run end, or a no-preflight override persists a durableembed_geometry_taintflag (lease-fenced) that keepsdoctornonzero until a verified full rebuild clears it.clawmem doctorgains the canary (section 10) and a sampled persisted-vs-fresh check on real index rows (section 11): fragments are reconstructed through the production parse/split/format pipeline and compared to their stored vectors. New vectors persist anembed_input_fp(SHA-256 of the exact embed input) enabling full validation; pre-0.21.0 rows validate structurally with title provenance flagged unavailable until their next re-embed. Definitive failures (fingerprint mismatch = stale input; fingerprint match + low cosine = corruption) exit nonzero immediately — sampling coverage can never mask them.
Embed-run survivability
The incident's remediation run died on a transient SQLITE_BUSY: the failure-marker write itself crashed a --force rebuild at doc 344/4,995 with the index already cleared. Now: the embed connection runs a 10s busy timeout (set on the ACTIVE connection, covering update --embed's cached store; kept short so synchronous waits cannot starve the 30s lease heartbeat), retries are asynchronous and bounded with a lease-loss abort between attempts, markEmbedStart/Synced/Failed are lease-fenced in-transaction, a marker that still fails logs-and-continues instead of killing the run, and --force skips the post-clear stale-embedding cleanup (a no-op that could only add a die-after-clear window).
Also
latestnow routes to recency intent (RECENCY_PATTERNS) — "latest decisions" was scoring under non-recency weights.Config knob
retrieval.mcp_direct_tuned_weights(default false; envCLAWMEM_MCP_DIRECT_TUNED_WEIGHTS): opt-in to score the MCP direct tools' non-recency queries with the retrieval-tunedquery-tool weights. The default flip is gated on a direct-pipeline eval — the existing n=199 evidence covered only the hybridquerypipeline.Read-only template A/B evaluator (
scripts/eval-query-template.ts): ranks known-target queries under query-template / doc-template / raw formatting against the live index through the same model+dimension guards as production, writing nothing. Measured post-incident: a doc-templated query ranked the true target #1 where the query template ranked it #383 — a query-side-only template change needs no re-embed.Production vector search now runs an explicit pre-MATCH dimension check via a shared query-vector compatibility guard (previously model-consistency only).
Docs: the zembed-1 launch line ships with
--pooling last --override-kv tokenizer.ggml.add_eos_token=bool:true(the missing flags seeded the incident); troubleshooting's claim that a re-embed is "not required" after a pooling fix is corrected (it IS required — same-dimension geometries are incompatible); the missing-EOS-anchor signature, shared-suffix diagnostic confound, compressed-high similarity bands, and watcher/teeoperational notes are documented.
Verification
bun test → 1492 pass / 0 fail (45 new regressions: escalation fill / cap-exhaustion markers / dedup-collapse / mixed-cause truthfulness / small-vault no-marker; traversal beam parity; shared-guard model+dimension; canary healthy/collapsed/drift/unavailable/mixed-endpoint; fail-closed preflight gate matrix; first-healthy baseline calibration; sampled-validation tiers incl. definitive-failure non-maskability, canonical-alias dedup, and hard attempt caps; busy-retry semantics; lease-fenced markers; latest routing; route-level MCP tests over an in-memory transport for all six retrieval tools incl. the incident composite-ranking fixture). Design adversarially reviewed to explicit clearance in a 7-turn cross-model DESIGN gate (34 findings folded); implementation review findings (fail-open canary gate, unbounded sampling, canonical-identity blindness, rolling baselines, taint persistence, and route coverage) folded before ship.
v0.20.2 — Beads sync hardening: argument-safe exec, telemetry-off spawns
runBd assembled a shell string and ran it through execSync, leaving argument interpolation to the shell, and bd v1.1.0 upstream turned on anonymous usage metrics by default with a remote reporting endpoint and a spawned flush sender — so every bd invocation ClawMem makes during a sync would have phoned home on upgraded installs.
execFileSyncreplaces the shell string (src/beads.ts): arguments pass as an array with no shell interpolation; same timeout, cwd, and error handling.Telemetry is disabled for ClawMem-spawned bd calls only. The spawn env forces
BD_DISABLE_METRICS=1andBD_DISABLE_EVENT_FLUSH=1. Older bd releases ignore the unknown variables (verified on v0.58.0); a user's own interactive bd keeps whatever metrics preference they chose — automated sync calls would only have skewed it.
Verification
bun test → 1447 pass / 0 fail. Empirical matrix on both ends of the supported bd range: v0.58.0 and v1.1.0 return identical rows with and without the env pair. The exec seam was flagged in an independent cross-model adversarial review (codex / GPT-5.5).
What didn't change
The parse schema, sync semantics, dep-type bridging, and document shape are untouched. v0.20.2 is byte-identical in behavior to v0.20.1 except for the exec mechanism and the spawned-call env.
v0.20.1 — Beads sync against bd v1.1.0: full-backlog list, dead field dropped, claim leases surfaced
An upstream delta survey of beads v1.0.5 → v1.1.0 (gastownhall/beads, formerly steveyegge/beads) found three drift points in the sync:
The 50-issue silent truncation is gone.
queryBeadsListinheritedbd list's default cap, so backlogs past 50 issues silently synced a prefix. The query now passes--limit 0(unlimited) — verified live against bd v0.58.0 and v1.1.0, so the fix does not raise the version floor.quality_scoredropped from the parse (src/beads.tsinterface, normalizer, and formatter). Upstream removed the field at v0.62.0; it wasomitemptyeven before, so realbd list --jsonoutput stopped carrying it long ago. This is bd's per-issue field — ClawMem's own indexing-time quality scoring is a different mechanism and is untouched.Claim leases surfaced. bd v1.1.0 issues can carry
lease_expires_at/heartbeat_at; the sync now parses both and renders a**Claim Lease**: expires …line when present, so agent-claimed work is visible in indexed memory. Absent on older bd → the line is skipped.
Verification
bun test → 1447 pass / 0 fail. No ClawMem consumer references the dropped field (store.ts / mcp.ts / CLI checked). --limit 0 and field behavior exercised against live databases on bd v0.58.0 and v1.1.0.
What didn't change
Dependency-type bridging is untouched — the new upstream dep types (tracks, until, authored-by, assigned-to, approved-by, attests) fall to the existing semantic default exactly as unmapped types always have. Watcher behavior, .beads/ discovery, and document format are unchanged apart from the two field-level items above.
v0.20.0 — Vector-query daemon: a hard cap on the cold synchronous MATCH
v0.16.0 and v0.17.0 bounded the context-surfacing hook's vector leg with wall-clock deadlines and kept the sqlite-vec payload warm with a watcher prewarm, but those are probability reductions: a synchronous bun:sqlite MATCH exposes no interrupt, so once a cold scan on a large vault is in flight it blocks the hook's event loop past the 8-15s budget and the in-thread Promise.race timer cannot fire. v0.17.0 tracked the true hard cap — moving the scan off the hook's event loop — as deferred. This release ships it.
Vector-query daemon, hosted by the watcher (opt-in, a pure optimization layer).
clawmem watchnow runs a per-vault unix-domain socket daemon. Thecontext-surfacinghook sends only the query string; the daemon runs Step 1 — the embed plus the blocking sqlite-vec MATCH — in its own process and returns the raw{hash_seq, distance}matches, which the hook hydrates locally (Step 2). With the blocking scan off the hook's event loop, the hook's realsetTimeoutfinally fires: a cold scan that would have blocked the turn now times out fast and falls back to FTS with bounded latency.searchVecis split intosearchVecMatch(Step 1) andhydrateVecResults(Step 2); the in-processsearchVeccomposes them, so its contract is unchanged, and both hook vector legs — primary and deep-escalation — are bounded, not just the first.Strict graceful degradation — never a dependency. When the watcher isn't running the socket is absent and the hook uses the in-process path exactly as before. When the daemon is busy or misbehaving the hook drops to FTS rather than re-running the scan in-process (which would reintroduce the block). A read-path model mismatch still surfaces as
VecReadModelMismatchError(warned once) across the socket, preserving the v0.18.0 contract.Single-flight, deadline-on-receipt, and a private socket. At most one scan runs per vault; a request arriving mid-scan gets an immediate
busy(→ FTS) rather than queuing, and a request whose deadline already elapsed is dropped without scanning — so cold-scan pileups cannot starve the watcher. The socket lives under$XDG_RUNTIME_DIR/clawmem/(0700 dir, 0600 socket), is keyed per vault DB path, refuses to clobber a live daemon from another watcher, and is unlinked on shutdown.CLAWMEM_VEC_TIMING=1logs per-leg outcome and elapsed for attribution.
Verification
bun test → 1447 pass / 0 fail — the project's release gate. New bug-first tests in tests/unit/vector-daemon.test.ts (16, deterministic under --rerun-each 3) cover the socket protocol (serve, malformed, oversized, teardown), single-flight and deadline-on-receipt, the per-vault socket derivation, and the client's full fail-open matrix (absent → in-process, busy/error → FTS, model-mismatch → typed rethrow); the Step-1 scan is dependency-injected so they run without an embedding server. A bare tsc --noEmit remains a non-gate for the reasons noted under v0.18.0; the new src/vector-daemon.ts and the split src/store.ts add no new type errors over that baseline. Reviewed by an independent cross-model adversarial pass (codex / GPT-5.5-high): a fresh DESIGN gate on the spec at build time, then code review to zero remaining findings.
What didn't change
Retrieval quality, scoring, ranking, and the vault format are untouched — the daemon returns the same Step-1 matches the in-process path would, hydrated by the same Step-2 query. A deployment that does not run clawmem watch gets byte-identical behavior to v0.19.0. The daemon hosts the general vault only; skill-vault vector queries stay in-process (a far smaller surface, not the ~2 GB risk).
v0.19.0 — Priority-based transcript formatting for session extraction
The decision-extractor and session-summary Stop hooks prepared their LLM input by walking the last N messages and truncating each to a per-role character cap until a flat budget ran out. Under that scheme a long run of mid-conversation tool output could exhaust the budget before the final assistant message (the actual outcome) was reached, and the original user request — the single most important anchor for extraction — carried the same weight as any other message. Extraction quality degraded on exactly the long, tool-heavy sessions where good observations matter most.
Priority-based transcript assembly.
prepareTranscriptnow classifies each message before budgeting: P0 the first user message (the original request), P1 the last real assistant message (the final response, skipping trailing tool calls), P2 tool activity, P3 the remaining conversation, P4 system messages. The critical P0/P1 pair is always included (at a doubled per-role cap); tool activity and then conversation fill the remaining budget and are truncated to fit rather than dropped wholesale; the result is reassembled in chronological order so the LLM still sees a coherent sequence. Tool detection keys off the generic[tool_use/[tool_resultmarkers, so a transcript that ends on a tool call correctly keeps the preceding assistant text as the final response instead of mislabeling the tool call as the outcome.
Verification
bun test → 1431 pass / 0 fail — the project's release gate. New bug-first tests in tests/unit/observer.test.ts cover classifyMessages (P0–P4 assignment, plus the end-on-tool and all-tool edge cases where no P1 exists) and prepareTranscript (P0/P1 always present, chronological order preserved, tight-budget prioritization, and tool messages truncated-to-fit rather than dropped). A bare tsc --noEmit remains a non-gate for the reasons noted under v0.18.0; the changed src/observer.ts adds no new type errors over that baseline. Reviewed by an independent cross-model adversarial pass (codex / GPT-5.5-high) to zero remaining findings.
What didn't change
Retrieval quality, scoring, the vault format, and every public API are untouched. The formatter keeps the same TranscriptMessage[] → string contract; only the selection and ordering of what survives truncation changed. A session short enough to fit the budget is formatted with the same content as before, now in guaranteed-chronological order.
v0.18.0 — Read-path embedding-model guard, extraction parrot-hardening, remote LLM/rerank auth
Three independent hardenings gathered from a QMD-upstream survey. The first is a behavior change on the query path (a new fatal that replaces silently-wrong results) and drives the minor bump; the other two are additive.
Read-path embedding-model consistency guard (contract change). A vault embedded with one model and then queried after the active embedding endpoint switched to a different model at the same dimension silently matched the new query vector against the old stored vectors — cosine-meaningless results that
VecDimensionMismatchErrorcannot catch (the dimension is unchanged).searchVecnow compares the endpoint-returned model against the vault's stored model(s) after the query embed and throwsVecReadModelMismatchErrorunless the vault holds exactly one model equal to the active one — a heterogeneous vault (more than one stored model) is rejected too, since the extra model's vectors still pollute the space. The comparison uses the endpoint's own reported model, not the caller's model alias, and is cached per connection keyed on SQLite'sdata_versionso a cross-processclawmem embed --forceinvalidates a stale verdict. Explicit query paths (MCP tools, the REST server, the CLI) surface the error; the fail-open hooks (context-surfacing, the Stop-hookdecision-extractor) warn once per process and degrade to BM25 rather than dropping the turn. Remedy:clawmem embed --force.Extraction prompts hardened against parroting. The conversation-synthesis and deductive-synthesis prompts carried copyable few-shot examples with concrete, real-looking content; a weak local extraction model run out of distribution echoed them verbatim instead of extracting. Both examples are replaced with structure-only
{{...}}skeletons, and a shared residue guard — extracted from the observer path intosrc/schema-placeholder.tsand now imported by all three extraction paths — rejects any output that echoes a schema placeholder or template marker. A newplaceholderRejectscounter surfaces echoed drafts in the deductive-synthesis stats. The guard deliberately does not blocklist the removed example text (plausible real facts like an OAuth decision would false-positive); it keys off the skeleton markers instead.Remote LLM + reranker authentication.
generateRemote(the remote LLM path) and the remote reranker path sent noAuthorizationheader, so neither could point at an authenticated cloud endpoint. New independent env varsCLAWMEM_LLM_API_KEYandCLAWMEM_RERANK_API_KEYadd aBearerheader when set, mirroring the existingCLAWMEM_EMBED_API_KEY. The three keys are independent (the services may sit behind different hosts). Additive and backward-compatible — no header is sent when a key is unset.
Verification
bun test → 1228 unit + 131 integration + 35 hooks = 1394 pass / 0 fail — the project's release gate. (A bare tsc --noEmit is not a gate here: the root tsconfig pulls in a vendored example app whose deps aren't installed, and the tree carries pre-existing type-loose test idioms; the changed W1/W2/W3 source adds no new type errors over that baseline.) New and expanded bug-first tests: tests/unit/embed-dimension-safety.test.ts (read-path model mismatch, endpoint-model-vs-caller-arg discrimination, heterogeneous-vault rejection, cross-connection data_version invalidation, and the fatal-rethrow helper), tests/unit/schema-placeholder.test.ts (residue detection with an explicit false-positive boundary), tests/unit/conversation-synthesis.test.ts and tests/integration/deductive-guardrails.integration.test.ts (skeleton echoes rejected, residue filtered), and tests/unit/llm-remote-config.test.ts + tests/unit/rerank-health.test.ts (auth headers present when configured, absent when not, keys independent). Reviewed across all three workstreams by an independent cross-model adversarial pass (codex / GPT-5.5-high) to zero remaining findings.
What didn't change
Retrieval quality, scoring, and the vault format are untouched. The read-path guard fires only on an actual model divergence — a correctly-embedded vault behaves exactly as in v0.17.0. When no auth keys are set, the LLM and reranker requests are byte-identical to before.
v0.17.0 — Harden the context-surfacing hook budget + cancellable embeds (follow-up to v0.16.0)
v0.16.0 fixed the dominant context-surfacing hook-timeout causes (the unbounded synchronous vector leg and the init-backfill write lock). This release closes the residual write-contention and embed-cancellation gaps on the same hook path, and keeps the warm-cache guarantee alive on long-running hosts.
Best-effort hook writes fail fast under contention. The hook's own writes (the dedup UPSERT,
context_usage, recall events, co-activations) are all best-effort, but the dedup UPSERT ran early and unguarded — a contendedSQLITE_BUSYthere aborted the whole hook before it could return context. It is now fail-open, and thecontext-surfacinghook process caps its ownbusy_timeout(1500ms) so a contended best-effort write fails fast instead of stalling the budget. The cap is scoped to that process only (the Stop hooks keep the 5000ms default), and the skill-vault opens on the hook path inherit the same cap.Cancellable embeds.
embed()now honors anAbortSignalend to end: the underlying fetch is cancellable, the 429 retry backoff aborts mid-sleep instead of sleeping through every retry, and an aborted embed is classified as cancellation — not a transport failure — so it no longer trips the 60s remote-down cooldown.searchVec/getEmbeddingderive the signal from their wall-clock deadline, and a deadline also suppresses the unbounded local-model fallback so a hook embed cannot start a model load past its budget. The indexing batch-embed path is unchanged.Periodic vector prewarm. The watcher's one-shot prewarm warms the OS page cache once; on a long-running host under memory pressure the kernel can evict the vector payload between hook calls, letting a cold synchronous scan creep back onto the hook path. The watcher now re-runs the embed-independent prewarm on an interval —
CLAWMEM_PREWARM_INTERVAL_MS, default 10 minutes,0disables, values below a 60s floor are clamped up — to keep the payload resident. This is a probability reduction, not a hard cap: a true bound on the uninterruptible synchronous scan needs process isolation and is tracked as deferred.Two test-methodology fixes (source proven correct, not adjusted). A pre-existing topic-boost fail-open test conflated the focus-topic variable with sequential recall-feedback state and read the real skill vault; it now uses two identically-seeded stores plus hermetic vault isolation, and the zero-match fail-open contract is proven byte-identical. A pre-existing watcher heavy-lane test set a
0..23quiet-window believing it meant "any hour," but the window is end-exclusive, so the test failed during the 23:00 local hour; it now omits the window (always-open) and a hermetic unit guard pins the end-exclusive boundary.
Verification
bun test → 1390 pass / 0 fail. New and expanded bug-first tests: tests/unit/hook-timeout-fix.test.ts (dedup fail-open under a held write lock, the named-vault busy_timeout cap, periodic prewarm firing + clean teardown, and the interval resolver's strict parse + floor) and tests/unit/llm-fallback.test.ts (embed AbortSignal across the fetch, the 429 backoff, the cooldown classification, and the local-fallback suppression). Each was guard-verified — it fails on the pre-fix source and passes after. Reviewed by an independent cross-model adversarial pass (GPT-5.5 high) to zero remaining findings.
What didn't change
Retrieval quality, scoring, and the vault format are untouched. The busy_timeout cap is scoped to the context-surfacing hook process, so the Stop hooks and every other command keep the operational default. On any host that does not run the watcher, behavior is exactly as in v0.16.0 (the periodic prewarm lives only in the watcher).
v0.16.0 — Fix: context-surfacing UserPromptSubmit hook intermittently times out
The context-surfacing hook could intermittently exceed its UserPromptSubmit budget ("hook timed out — output discarded"), especially on the first prompt after a fresh boot and across concurrent sessions. The dominant cause was not inference or host memory: the vector leg ran a synchronous sqlite-vec scan that the Promise.race(vectorTimeout) guard could not bound (a synchronous call blocks the event loop, so the timer never fires), and every writable hook open ran an unconditional backfill UPDATE that could wait out busy_timeout under writer contention.
Bounded vector search on the hook path.
searchVecnow takes a wall-clock deadline and self-aborts before the blockingMATCHif the budget elapsed during the async embed. Both the balanced and deep-escalation vector legs race the embed against the remaining budget and clear their timers, so a pending timer no longer keeps the hook process alive after results are in hand.No write lock on a healthy init. The
last_accessed_atbackfill is read-guarded (skipped when nothing needs it) andinitializeDatabase'sbusy_timeoutis capped to the caller's value, so a writable hook open no longer waits out the initbusy_timeoutunder contention.Watcher-side vector prewarm. A single embed-independent prewarm (a zero-vector
MATCH) warms the sqlite-vec payload into the OS page cache on watcher startup so the first post-boot hook call isn't cold. It runs only in the watcher process (never per-session), reports success only when a scan actually ran, and never blocks startup.
Cross-model reviewed (GPT-5.5, five rounds to zero findings). New tests: tests/unit/hook-timeout-fix.test.ts.
v0.15.1 — Fix: macOS bootstrap fails to load the sqlite-vec extension (Issue #20)
On macOS, clawmem bootstrap (and clawmem doctor) failed at the database step with This build of sqlite3 does not support dynamic extension loading. Apple's built-in SQLite — which Bun uses by default — is compiled without extension-loading support, so the sqlite-vec vector extension cannot load. The prior macOS handling probed only the Apple-Silicon Homebrew path and swallowed the failure silently, so a fresh macOS install with no brew install sqlite (and Intel Macs, whose Homebrew prefix differs) hit the bare extension-loading error with no guidance. Yoloshii/ClawMem#20.
What changed:
Broader extension-capable SQLite detection (
src/store.ts):setCustomSQLite()now probes the Apple-Silicon (/opt/homebrew) and Intel (/usr/local) Homebrew prefixes, falling back tobrew --prefix sqlitefor non-standard prefixes (only when the standard paths are absent, so the common case pays no subprocess cost). Every candidate is existence-checked before use —setCustomSQLite()with an invalid path hard-crashes Bun (oven-sh/bun#18811), so the existence guard is load-bearing, not cosmetic.Actionable error instead of the cryptic one (
src/store.ts): bothsqlite-vecload sites now route through a helper that, on macOS, rewrites the "does not support dynamic extension loading" failure into guidance to runbrew install sqlite(naming the detected SQLite path, or noting none was found).clawmem bootstrapandclawmem doctorsurface this message directly instead of the bare extension error.Troubleshooting entry (
docs/troubleshooting.md→ "Bun runtime"): documents the symptom, thebrew install sqlitefix, and the auto-detection behavior.
Verification
bun test tests/unit/ → 1183 pass / 0 fail. A new bug-first test (tests/unit/store.macos-sqlite.test.ts, 5 cases) asserts the error mapping: macOS + no Homebrew SQLite → brew install sqlite guidance; macOS + a detected-but-failing SQLite → brew reinstall + the path; non-macOS and unrelated errors pass through untouched. It fails on the pre-fix source (no mapping existed) and passes after.
What didn't change
No change to retrieval, scoring, the vault format, or any non-macOS code path — on Linux/Windows the macOS detection block is skipped entirely and
sqlite-vecloads exactly as before. This is a macOS-only install fix.
v0.15.0 — Agent-instruction refactor (AGENTS.md as lean SSOT) + antipattern durability fix
The agent-facing instruction surface was three overlapping copies — CLAUDE.md and AGENTS.md were byte-identical 72 KB / 800-line twins (2.2× over Codex's 32 KiB AGENTS.md cap, which truncates silently), and SKILL.md was an 830-line third copy. All three duplicated each other and the existing docs/ tree. This release aligns to the convention — AGENTS.md is the lean root SSOT, CLAUDE.md imports it, SKILL.md is on-demand operational guidance, and the deep reference lives in docs/. It also fixes a latent scoring bug found during review: antipattern memories were decaying despite being documented and intended as durable.
What changed:
AGENTS.mdis now a lean root SSOT (72,637 → ~17.7 KB, under the 32 KiB cap). Keeps the agent-facing essentials — inference-at-a-glance, install, the 90/10 retrieval model + Tier-2 hook table, the 3-rule escalation gate, Tier-3 tool routing + MCP tool table, query-optimization levers, composite-scoring summary, indexing rules, lifecycle, anti-patterns, integrations — and points intodocs/for everything deep, with a reference index at the foot.CLAUDE.mdis now an@AGENTS.mdimport (72,637 → 379 bytes), ending the byte-identical twin and its dual-maintenance drift. Claude Code readsCLAUDE.mdnatively, so the import bridges to the single SSOT.SKILL.mdtrimmed to an on-demand operational reference (830 → ~267 lines,version2.0.0): escalation gate, tool routing, the 4 query-optimization levers, pipeline behavior, composite scoring, lifecycle, gotchas — with repo-relative pointers. Setup / inference / config / internals deliberately live inAGENTS.md+docs/.New
docs/guides/inference-services.mdconsolidates the inference / model / server-setup content that was triplicated acrossAGENTS.md, the README "GPU Services" wall, andcloud-embedding.md, with a stack-decision matrix (QMD-native vs SOTA z-stack vs cloud embedding) up front.New
docs/reference/configuration.md— a complete environment-variable reference (everyCLAWMEM_*var except the internal, process-setCLAWMEM_STDIO_MODE).README: the "GPU Services" setup wall collapsed to a ~20-line decision callout linking the new guide (81.5 → 71.5 KB); the "Agent Instructions" file-roles table updated;
docs/guides/systemd-services.mdgained the scheduledclawmem-rerank-healthunit (previously documented only in the old AGENTS.md).package.jsonfiles[]now includesdocs/so the relativedocs/pointers in the shippedAGENTS.md/CLAUDE.md/SKILL.mdresolve for npm consumers, not just git clones.Fix —
antipatternmemories are now durable (src/memory.ts):antipatternwas inDECAY_EXEMPT_TYPESand mapped to asemanticrelation, but was omitted fromHALF_LIVESandTYPE_BASELINES, so it fell through to the 60-day half-life / 0.5 baseline defaults — i.e. it decayed despite being documented as ∞ half-life / 0.75 baseline. Addedantipattern: InfinitytoHALF_LIVESandantipattern: 0.75toTYPE_BASELINES(matching the documented values). Accumulated negative patterns now persist as intended and rank with a durable baseline.
Verification
bun test tests/unit/ → 1178 pass / 0 fail. A new bug-first test (tests/unit/memory.scoring.test.ts) asserts antipattern durability — recencyScore returns 1.0 at one year old, antipattern confidence outranks note (exercising the 0.75 baseline), and the attention-decay exemption holds; it fails on the pre-fix source and passes after. Every relative link in the refactored files was resolved; AGENTS.md confirmed under 32 KiB; CLAUDE.md confirmed no longer a byte-twin. Reviewed by an independent cross-model adversarial pass (codex / GPT-5.5-high) across the refactor and the source fix to zero remaining findings.
What didn't change
No retrieval-pipeline, scoring-formula, or vault-format change beyond the
antipatterndurability fix. Documented facts, tool routing, hook behavior, and scoring weights are preserved — moved, condensed, or consolidated, not altered.The only runtime behavior change is
antipatternmemories no longer decaying (and getting a 0.75 vs 0.5 baseline); every other content type's half-life and baseline is unchanged.
v0.14.0 — Reranker health guard: detect a silently-degenerate reranker (doctor + scheduled check + runtime emit)
v0.11.3 deprecated the broken zerank-2 GGUF and shipped a faithful sidecar; v0.12.0 made the reranker the dominant ranking signal. Together they raised the stakes of a silent reranker failure: the broken GGUF returned HTTP 200 + valid JSON + finite positive ~1e-11 scores — passing every liveness check — yet contributed nothing at weight 0.9 and silently collapsed the final ranking to RRF. blendRerank's usability check was score > 0, which those ~1e-11 scores passed, and clawmem doctor had no reranker probe at all. This release makes that failure mode impossible to miss.
What changed:
blendRerankdegenerate-floor trip + visible fallback (src/search-utils.ts): the usability check widened from> 0to> RERANK_DEGENERATE_FLOOR(1e-4) — above the broken regime's 8e-7 ceiling, far below the weakest working score (~0.1) — so a near-zero collapse now routes to the RRF fallback instead of blending in ~nothing. The 3rd arg accepts an options object{ rerankWeight, degenerateFloor, onFallback }(numeric back-compat preserved); thequerycaller (src/mcp.ts) passes anonFallbackthat emits a rate-limited (≤1/min) stderr warning + running count, so the previously-silent degrade is surfaced.clawmem doctorsection 9 — reranker discrimination (src/clawmem.ts): an active probe that asserts the reranker discriminates, not just responds. Runs a shipped golden set of same-topic (query, relevant, hard-negative) pairs (src/health/rerank-golden.json) through the live reranker, cache-bypassed, and checks coverage + a calibration band + a per-pair discrimination margin.clawmem rerank-healthcommand + scheduled unit (src/clawmem.ts;CLAUDE.md/AGENTS.md): the same probe as a standalone command that exits non-zero on degeneracy, for a systemdOnFailure=alert (clawmem-rerank-health.{service,timer}, documented) — proactive detection of an endpoint reverted to the broken GGUF, independent of query traffic.store.rerankprobe seam (src/store.ts): an additive{ noCache, requireLiveCoverage, signal, timeoutMs }options param.noCachebypasses the rerank cache (so a probe always exercises the live endpoint);signal/timeoutMsbounds the remote fetch;requireLiveCoverageenforces the full coverage contract — exactlybatch.lengthresults, unique in-range integer indices, finite numeric scores, a valid JSON object body — before the score-apply zero-fill (after which an omitted score and a true 0 are indistinguishable), throwingRerankCoverageError/RerankMalformedResponseError. A defensive skip in the apply loop also hardens the production path against an out-of-range index or non-array body (previously a latent crash).
Verification
Thresholds were calibrated from a live zerank-2-seq baseline (8-pair golden set): relevant scores 0.92–0.97, hard-negative ≤ 0.31, minimum margin 0.64, 0/8 inverted — vs the broken GGUF regime's max-ever 8.03e-7 (a 5–6 order-of-magnitude separation). Locked: CALIB_FLOOR 0.05, DISCRIM_MARGIN 0.25 (2.5× below the live minimum margin), RERANK_DEGENERATE_FLOOR 1e-4. 24 new unit tests (tests/unit/rerank-health.test.ts) pin the load-bearing contracts: the degenerate-floor trip + onFallback, the options-object overload + 2-arg back-compat, coverage-before-zero-fill, the malformed-response contract (duplicate / out-of-range / wrong-count / non-numeric / invalid-JSON / null-body), the defensive non-probe skip, and the calibration-band-passes-but-margin-fails (constant-output) case. Full suite: 1363 pass / 1 pre-existing unrelated failure; tsc --noEmit clean. Reviewed across the design and the implementation by an independent cross-model adversarial pass (codex / GPT-5.5-high) to zero remaining findings.
What didn't change
The healthy hot path is untouched. A working reranker scores ≫ 1e-4, so
blendRerankbehaves exactly as in v0.12.0; the degenerate floor only changes behavior when the reranker has already collapsed (where the old code silently produced RRF order anyway — now it is explicit and surfaced).store.rerank's no-options path (thequery,intent_search, and context-surfacing callers) is byte-identical to before.No schema migration, no config/env-var change, no breaking API change — the
store.rerankoptions param and theblendRerankoptions object are additive, and the new CLI command,doctorsection, golden set, and systemd recipe are all additive. The default reranker stays qwen3-reranker-0.6B.
v0.13.0 — Query composite re-weight: search 0.70 for the query tool (the deferred lever from v0.12.0)
v0.12.0 fixed the rerank blend but flagged a larger deferred lever: composite scoring's default {search:0.50, recency:0.25, confidence:0.25} puts half the weight on non-search signals, which caps how much the improved blend reaches the surfaced top-k. v0.12.0 deferred acting on it "pending a judged-relevance / recency-aware eval." This release runs that eval and acts on it — scoped to the query tool only.
What changed:
The
querytool now scores withQUERY_WEIGHTS = {search:0.70, recency:0.15, confidence:0.15}(src/memory.ts), replacing the 0.50/0.25/0.25 default for non-recencyquerycalls. Implemented via an additive{ weights, now, forceWeights }options seam onapplyCompositeScoring; thequerycall passes{ weights: QUERY_WEIGHTS }withoutforceWeights, so a recency-phrased query still switches toRECENCY_WEIGHTS(0.10/0.70/0.20) by construction. Scoped to thequerytool —search,vsearch,memory_retrieve's routed modes,intent_search, and the context-surfacing hook keep the 0.50/0.25/0.25 default (their pipelines weren't part of this eval).
Verification
A held-out, judged-relevance eval — the recency-aware eval v0.12.0 asked for. Real query-shaped prompts (held-out n=279 → 199 usable after oracle-drop) were graded 0–3 by an LLM judge (GLM-5.2) over the full ~30-doc candidate pool per query, blind / order-shuffled / freeze-time-framed. The judge was calibrated against an independent annotator (quadratic-weighted κ=0.681) plus an order-perturbation self-consistency check (κ=0.77). A dev pilot (n=84) picked the candidate weight; a pre-registered decision rule (locked before the held-out judging) then chose between 0.70 and 0.80. Result: graded NDCG@10 vs the 0.50 control — w_search 0.70 +0.064 (paired permutation p<1e-4), 0.80 +0.104, robust across precision / exploratory / temporal families. A dedicated freshness guard (does raising search weight demote the newest-correct version of an evolving doc?) was the tiebreaker: at 0.70 the newest-correct doc is never demoted (mean rank improves); at 0.80 it falls out of the top-10 in 2/19 supersession cases. So 0.70 captures the bulk of the gain with zero freshness regression, while 0.80's extra NDCG came at a freshness cost in a work-memory vault full of evolving docs. Pinned- and revision-rank-stability guards are non-regressive at 0.70. Reviewed across design, dev results, hardening, and implementation by an independent cross-model adversarial pass (codex / GPT-5.5-high) to zero remaining findings.
What didn't change
RECENCY_WEIGHTSand the recency-intent switch — a recency-phrased query ("latest", "recent", "last session", …) is scored exactly as before. The freshness guarantee the eval relied on holds by construction (forceWeightsis never set).search,vsearch,memory_retrieverouted modes,intent_search, and the context-surfacing hook — all keep the 0.50/0.25/0.25 default. The seam's no-options path is byte-identical to prior behavior.No schema migration, no config/env-var change, no API-shape change.
queryresult ordering shifts, and the composite-score distribution shifts upward for search-dominated hits — threshold consumers of the returnedcompositeScore(e.g.minScore) may see changed inclusion near the boundary. Scores remain[0,1]-scaled.
v0.12.0 — Query reranking: blend the cross-encoder as the dominant signal (fixes the immovable RRF #1)
The query tool fused the cross-encoder reranker into the final ranking with a position-aware blend — rrfWeight·(1/rrfRank) + (1-rrfWeight)·rerankScore, with rrfWeight 0.75 / 0.60 / 0.40 by RRF rank. Because reranker scores are in [0,1], RRF rank-1's floor (0.75·(1/1) = 0.750) exceeds RRF rank-2's ceiling (0.75·(1/2) + 0.25·1 = 0.625): RRF #1 was mathematically immovable by the reranker. A strong reranker could reorder the tail but could never promote the best document to the top. With the now-faithful zerank-2 seq-cls reranker (v0.11.3), that ceiling was discarding the reranker's single largest win.
What changed:
New
blendRerankblend (src/search-utils.ts):0.1·normalizedRRF + 0.9·rerank. The cross-encoder is the dominant relevance signal; normalized RRF is a thin tiebreaker — so a strong rerank score can promote a document over RRF #1. It maps over the candidate set (so partial rerank coverage can never drop a candidate) and falls back to pure RRF order when the reranker is unavailable or returns no usable signal (empty / all-zero — e.g. a total remote+local failure, now also caught by a try/catch around the rerank call). Scoped to thequerytool;intent_searchis unchanged (its blend uses actual upstream scores, a different shape, measured separately).
Verification
Harness-validated against known-item recall over two eval sets (NL n=45, KW n=50) on a frozen 3,743-doc snapshot with the live reranker, faithfully replicating the production query path (the harness's pre-rerank ranking reproduces the prior pipeline's exactly). At the blend stage the reranker lifts recall@1 from 0.22 (RRF alone) to 0.62 — and the old blend discarded all of it (blend@1 equalled RRF@1 to three decimals). The shipped 0.1/0.9 blend improves final recall@1–5 and MRR@10 on both eval sets with no material pooled recall@10 regression (NL @10 +0.044, KW @10 −0.04, pooled tie). New unit tests (tests/unit/search-utils.blend.test.ts) pin the two load-bearing contracts: a strong rerank score promotes above RRF #1, and empty/all-zero rerank preserves RRF order. Reviewed across design, results, and implementation by an independent cross-model adversarial pass (codex / GPT-5.5-high) to zero remaining findings.
What didn't change
intent_search,search,vsearch, the context-surfacing hook's deep-profile rerank blend, composite scoring, and MMR are unchanged — only thequerytool's rerank/RRF blend. No schema migration, no config/env-var change, no API-shape change (result ordering forqueryshifts; scores remain[0,1]-scaled as before).A separate, larger lever surfaced by this work — composite scoring's 50% non-search weighting, which caps how much the improved blend reaches the surfaced top-k — is deferred to a future release pending a judged-relevance / recency-aware eval (known-item recall alone can't adjudicate it). MMR was measured to be a near-no-op here and is left untouched.
v0.11.3 — Reranker: deprecate the broken zerank-2 GGUF; ship the zerank-2 seq-cls sidecar (SOTA, non-commercial)
The "SOTA upgrade" reranker — zerank-2-Q4_K_M.gguf served under llama-server --reranking — was silently broken. This release deprecates it across the docs and ships a working replacement as an opt-in recipe.
Root cause: zerank-2 is a Qwen3ForCausalLM that scores a (query, document) pair on the logit of a single relevance token ("Yes", id 9454) via a sentence-transformers LogitScore head. llama.cpp's convert_hf_to_gguf.py only synthesizes a rerank head when the model card contains the literal string # Qwen3-Reranker; zerank-2's card lacks it, so the previously-recommended GGUF — and any built by the current/standard llama.cpp converter — is a headless causal LM. Served with --reranking it returns near-zero, uninformative scores → reranking degrades to an inert RRF-dominated passthrough, with no error.
What changed:
New opt-in recipe at
extras/rerankers/zerank-2-seq/— convertszeroentropy/zerank-2-rerankerto aQwen3ForSequenceClassification(num_labels=1) whose score head is the tied-embedding row 9454, so the relevance logit is identical by construction to the native causal score. Served as a small transformers sidecar (/v1/rerank,batch=1, applies zerank's chat template, returnssigmoid(logit/5)) behind the existingCLAWMEM_RERANK_URLcontract — drop-in, no ClawMem code change.Reproducible correctness gate (
build_and_verify.py) — the convert step refuses to finish unless it proves: fp32 score-head weight-equality (bf16 preserved, no fp16 downcast); served-tokenizer == source-tokenizer (including the truncation path); the assistant-generation prefix survives near-MAXLENinputs; and the seq-cls logit equals the causal token-9454 logit bit-exactly over the real served path (including batched right-padded pooling and empty/whitespace-doc edges).Docs corrected —
README.md,CLAUDE.md/AGENTS.md,SKILL.md,docs/quickstart.md,docs/introduction.md, anddocs/guides/cloud-embedding.mdnow point the SOTA reranker at the sidecar and explain the GGUF deprecation;docs/guides/upgrading.mdgains a migration section. Full-SOTA-stack VRAM guidance moves 12GB → 16GB (the bf16 reranker is ~9GB).
Verification
The conversion's correctness is enforced by build_and_verify.py, which the convert step runs before serving and which exits non-zero unless every gate passes: fp32 score-head weight-equality (bf16 preserved, no fp16 downcast); served-tokenizer == source-tokenizer identity (including the truncation path); assistant-prefix preservation under near-MAXLEN inputs; and seq-cls-vs-causal token-9454 logit equivalence over the real served path (batched right-padded pooling + empty/whitespace-doc edges). Verified live after deploy: relevant vs. irrelevant scores 0.96 / 0.08, matching the gate.
What didn't change
zembed-1 (SOTA embedding) and qwen3-reranker-0.6B (default reranker) are unchanged — only the zerank-2 reranker GGUF is deprecated.
No
src/change, no schema migration, no config/env-var change, no public API change. ClawMem's default reranker stays the permissively-licensed qwen3-reranker-0.6B; the sidecar is an opt-in upgrade. zerank-2 weights are CC-BY-NC-4.0 (non-commercial) and are never bundled — the recipe downloads them for your own use.
v0.11.2 — Packaging: bin path so npm install -g registers the clawmem command on npm 11
v0.11.2 is a packaging-only fix. The bin map declared "clawmem": "./bin/clawmem"; npm 11's publish path rejects the ./-prefixed form and drops the bin entry from the tarball (older npm silently rewrote it — the live v0.11.0 ships bin/clawmem), so a global install would land the files but expose no clawmem command. Changed to "clawmem": "bin/clawmem", verified with npm publish --dry-run (no warning; the bin survives in the packed package.json).
No source change from v0.11.1. v0.11.1 was git-tagged but never reached npm (the publish that surfaced this packaging issue also failed on an expired npm token), so on npm v0.11.2 supersedes v0.11.0 directly and carries the full v0.11.1 query-expansion fix documented below.
v0.11.1 — Query expansion: typed lex/vec/hyde routing + terse qmd prompt (fixes garbage / mis-routed expansions)
v0.11.1 fixes the LLM query-expansion stage, which had two compounding bugs that quietly degraded query / intent_search recall: the expansion model produced garbage variants, and the variants that were usable got routed to the wrong search backend.
Root cause — two layers:
Prompt was out-of-distribution for the finetune.
expandQueryRemote(src/llm.ts) sent a verbose prose instruction with no format constraint. The shipped expansion model (qmd-query-expansion-1.7B, a Qwen3-1.7B finetune) was trained on QMD's terse/no_think Expand this search query: <q>form. The mismatch produced bare question-stem "lex" terms ("What are the", "How do the"), literal template echoes, and</think>leakage — roughly a third of queries returned pure template noise.Variant type was erased at the routing layer. The expander emits typed
lex(keyword),vec(semantic), andhyde(hypothetical-answer) variants, but every consumer dropped the type and searched each variant on both backends — so keyword expansions were vector-searched and hypothetical-answer passages were BM25-searched, each leg fed the input it handles worst. The CLI (clawmem query) was worse: it re-parsed already-strippedlex:/vec:prefixes, collapsing every variant into a single mis-typed list.
What changed:
Terse, in-distribution prompt.
expandQueryRemotenow sends/no_think Expand this search query: <q>(plus the intent line when provided), matching the finetune's training form. Clean lex/vec/hyde verified end-to-end against the live server.Typed
ExpandedQuerycontract end-to-end.store.expandQueryreturns{ type, query }[]instead of erasing the type, and every consumer routes by it:lex → BM25,vec/hyde → vector, original query → both backends (the only leg that fans out to both, keeping its 2× RRF anchor). Fixes the MCPquerytool (src/mcp.ts), the CLI (src/clawmem.ts), and the deep-escalation path incontext-surfacing(src/hooks/context-surfacing.ts). The MCP tool now also counts the original query's actual contributed list count for the 2× positional weight, fixing a latent mis-weight when the original's BM25 or vector leg came back empty.Shared sanitize guards. A single
sanitizeExpandedQueries(src/llm.ts) runs in both the remote parser and the store wrapper: strips stray control tokens (/no_think,/think, word-boundary-safe), rejects template-residue /<think>/ empty lines, dedups, and echo-filters variants equal to the original. A typedexpansionFallbackcovers expansion failure or all-junk output, andisFallbackExpansiondetects a leaked llm-level fallback so the store returns expansions-only and does not cache it.Versioned expansion cache. Cache key bumped to
expandQuery:v3-qmd-terse-typedwith a provider fingerprint and typed-JSON value; the old newline-delimited cache ages out via LRU (no manual purge). A malformed v3 entry is rejected and re-expanded instead of partially accepted.
Verification
tsc clean on the touched files; full unit suite green (187 pass). Live end-to-end against the running expansion server: typed shape, zero echo, zero template-junk, correct per-type routing, cache round-trip. Validated under a fresh GPT-5.5 high-reasoning adversarial review via codex exec (a separate impl-review session from the design session): Turn 1 surfaced three real findings — a leaked llm-level fallback being cached, partial acceptance of a malformed typed-cache entry, and CLI expansion-only candidates dropped after RRF — each fixed and re-verified to verbatim "zero remaining findings."
What didn't change
Composite scoring, MMR diversity, cross-encoder reranking, RRF fusion weights, vault format, hook set, and the agent tool surface are unchanged.
No schema migration, no config or env-var change, no public API change; the expansion model and the three inference services are the same. One behavior change: query expansion now routes each variant to a single backend by type instead of searching every variant on both — recall improves on the same vault the moment you upgrade. Pure
bun add -g clawmemupgrade.
Docs docs/internals/query-pipeline.md, AGENTS.md, and CLAUDE.md updated to describe typed routing (AGENTS.md / CLAUDE.md also carry a pending correction: MPFP meta-path fusion is max-score, not RRF).
v0.11.0 — Embedding dimension-migration safety + lease-fenced, atomic vector writes
v0.11.0 hardens the embedding write path against a class of silent vector loss surfaced by a real incident: a re-embed reported success, but pure-vector retrieval (vsearch / find_similar) returned nothing for the affected docs. Root-cause analysis found two layers — a model-serving quality issue (operator-side; see Troubleshooting → "weak or irrelevant results") and, in ClawMem itself, an unsafe dimension migration: when the embedding model's output dimension changed, ensureVecTable dropped the vectors_vec table while the metadata-based worklist (getHashesNeedingFragments) skipped the now-vectorless documents (their content_vectors rows still existed). The result was a vault whose vectors were silently wiped while embed reported "all done."
What changed in the embedding write path:
ensureVecTablenever drops an existing table. A dimension/schema mismatch now throws a fatalVecDimensionMismatchErrorand aborts the run instead of dropping. The only path that clears vectors is the explicitclearAllEmbeddings, reached only viaembed --force.embeddetects dimension AND model drift non-destructively. An implicit (non---force) run that sees a changed dimension — or a different model at the same dimension, which mixes a heterogeneous, similarity-meaningless vector space — aborts with instructions to run--force, never mutating.embed --forceprobes the endpoint first and aborts without clearing if it's unreachable, so a force rebuild against a dead server cannot wipe the vault. The whole run is bound to one(dimension, model); every embedding is validated before it is stored.All vector mutations are atomic and lease-fenced.
insertEmbedding,clearAllEmbeddings,cleanStaleEmbeddings, and table creation each run in a single immediate-write-lock transaction that verifies a renewable, token-fenced embedding lease (worker_leases, nameembedding, heartbeat-renewed) before mutating — so two concurrent embeds, or a process that lost its lease mid-run, cannot interleave a clear with an insert or mix two models into one index. A second concurrentembedskips cleanly.Crash-safe retry budget.
embed_attemptsincrements exactly once per attempt (at start), resets on a successful embed, and resets whenever a document's content changes (new hash) via a newreset_embed_on_hash_changetrigger that covers every hash-changing path. A document can no longer be permanently excluded by stale failures from old content. Partial embeds are retried in full.doctornow reports content_vectors ↔ vectors_vec consistency (a set-difference check, including the worst case wherevectors_vecis absent but metadata rows remain), and flags a vault that contains mixed embedding models.embedexits non-zero when a run aborts (dimension/model mismatch or lost lease), so the embed timer /update --embedcan detect an incomplete run.
Verification
Validated across a 9-turn GPT-5.5 high-reasoning adversarial review under codex exec: a diagnosis pass, four design passes (which overturned an initial "defer the concurrency lease" decision by demonstrating that two same-dimension models can silently build a heterogeneous index), and four code-review passes that drove findings 6 → 4 → 3 → 2 → 0 ("vector-table mutations are now atomic, lease-fenced, and dimension/model consistent"). Ships with new unit tests covering throw-on-mismatch, getVecTableDim states, the lease fence on insert/clear, the hash-change trigger, attempt-budget resets, and getVecModels heterogeneity detection; full suite green except one pre-existing unrelated integration test.
What didn't change
Retrieval pipeline, composite scoring, vault format, hook set, agent tool surface, and OpenClaw/Hermes plugin registration are unchanged.
No public API change. One behavior change:
embednow aborts on a dimension/model change instead of silently re-embedding into a dropped table — runclawmem embed --forceto migrate. The only schema addition is thereset_embed_on_hash_changetrigger, created automatically on store open (no manual migration).
v0.10.7 — Hermes plugin: refresh session-derived state on on_session_switch
v0.10.7 implements the MemoryProvider.on_session_switch hook in the Hermes plugin (src/hermes/__init__.py). Hermes Agent (v2026.5.16) wired this lifecycle hook to fire on /new (reset=True), /resume, /branch, and context compression — any mid-process session_id rotation that does not tear the provider down. ClawMem previously did not override it (the ABC default is a no-op), so after a switch the plugin kept using the session_id it cached at initialize(): extraction and handoff metadata carried the stale id, and the session-keyed transcript file ({session_id}.jsonl) kept collecting the new session's turns under the old name.
The override repoints the cached _session_id, rebuilds _transcript_path for the new session, and unconditionally invalidates the prior session's prefetch + bootstrap caches so a recall queued under the old session cannot surface in the new one. To stay race-free with the background prefetch worker, queue_prefetch now snapshots the session id and transcript path under the prefetch lock at queue time (the worker uses the snapshot, never live state), and the switch bumps the prefetch generation monotonically so an in-flight worker discards its result instead of writing it into the new session. Retrieval is unaffected — the vault is path-keyed, not session-keyed — so this is metadata/transcript correctness, not a recall change.
Verification
Validated across three turns of GPT-5.5 high-reasoning adversarial review under codex exec. Turn 1 (design) returned no-ship-as-written and caught two real bugs in the initial design — a reset-gated prefetch leak (stale recall crossing into /resume and /branch sessions) and an ABA race from resetting the prefetch generation to zero — and prescribed the snapshot-at-queue-time fix. Turn 2 verified the implemented design against a standalone behavioral test covering the switch / reset / compression / prefetch-race paths → verbatim "zero remaining design findings — can ship." Turn 3 re-cleared a final added assertion.
What didn't change
Retrieval pipeline, composite scoring, vault format, hook set, agent tool surface, OpenClaw plugin registration, and the Claude Code hook path are all unchanged from v0.10.6.
No reindex, no schema migration, no public API change, no config or env-var change. Pure
bun add -g clawmemupgrade.
v0.10.6 — FTS keyword-search tokenization: split separators instead of stripping them
v0.10.6 fixes an internally-found bug in the BM25 keyword-search path: the query builder silently returned zero rows for any compound / path / snake_case / dotted / hyphenated query — exactly the config-name, hook-name, filename, and identifier searches the system is meant to serve.
src/store.ts:sanitizeFTS5Term() removed every non-alphanumeric character from each whitespace token, which concatenated separator-delimited word-parts into a token that was never indexed. The FTS index (documents_fts, tokenize='porter unicode61') does the opposite — it splits stored text on _ - . / ' and all punctuation. So query and index tokenization disagreed:
before_compaction→beforecompaction→MATCH "beforecompaction"*→ 0 rows (the index holdsbeforeandcompactionas separate tokens)src/store.ts→srcstorets→ 0 rows;q4_k_m/v0.8.2→q4km/v082→ 0 rows
Vector recall partially masked this in the hybrid query path, but pure search (BM25-only) and the raw file-path supplemental lookup in context-surfacing returned nothing. CLAUDE.md, AGENTS.md, and SKILL.md already documented "code identifiers work" — this release makes the implementation match that promise.
Vault on disk is unchanged. No reindex required — a query-build change only, so it fixes every existing vault the moment you upgrade. No schema migration, no env-var change, no public API change. Pure bun add -g clawmem upgrade.
The fix — split, don't strip
sanitizeFTS5Term is replaced by tokenizeForFTS5, which splits on the same boundaries the index tokenizer uses:
export function tokenizeForFTS5(query: string): string[] {
return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(t => t.length > 0);
}buildFTS5Query keeps the existing AND-of-prefixes semantics ("before"* AND "compaction"*). One-character tokens are kept (needed for q4_k_m, v0.8.2, a/b); AND makes them a precision constraint, not noise. The split regex consumes every FTS5 syntax character, so each surviving token is alphanumeric and quoted — at least as injection-safe as the prior strip approach.
Entity FTS — exact-first candidate gathering
The same tokenizeForFTS5 is shared with the two entities_fts MATCH builders (src/entity.ts). Adversarial review surfaced that a naïve prefix query there starves short exact names: entities_fts applies its LIMIT before the Levenshtein / mention-count ranking, so a broad prefix ("go"* matching 30 Golang* rows, or "c"* for C++) fills the candidate pool and drops the exact row before it can be ranked. resolveEntityCanonical and searchEntities now gather exact-token matches first, then top up with deduped prefix matches only while under the limit (gatherEntityFTSCandidates). The exact row is always present for ranking; multi-char prefix recall (clawme* → clawmem) is preserved as a supplement.
Verification
Validated across four turns of GPT-5.5 high-reasoning adversarial review under codex exec. Turn 1 cleared the approach; Turn 2 confirmed the documents_fts fix correct + injection-safe but caught the entity prefix-starvation regression; Turn 3 rejected a 1-char-only band-aid (the same starvation hit short multi-char names like Go); Turn 4 cleared the exact-first fix verbatim "zero remaining findings" after re-running the repros (C++ resolves, Go resolves, clawme→ClawMem recall holds).
Test coverage: 8 new bug-first tests in tests/integration/store-search.test.ts (compound, non-adjacent AND, slash-path via the filepath column, 1-char tokens, apostrophe behavior-lock, FTS5-specials no-throw, punctuation-only → empty) + 4 entity starvation regression tests in tests/unit/entity.test.ts (C++ and Go, each across resolveEntityCanonical and searchEntities). Five of the store tests fail on the pre-fix code; all pass after. Full suite: 1298 pass / 0 fail. tsc --noEmit clean for the changed files.
What didn't change
Retrieval pipeline shape, composite scoring, vault format, hook set, agent tool surface, OpenClaw plugin registration (
kind: memory), and the Hermes plugin contract are all unchanged from v0.10.5.No reindex, no schema migration, no public API change, no config or env-var change.
The
store ⇄ entityimport added for the shared tokenizer is runtime-safe (hoistedfunctiondeclaration, called only at runtime).
Cross-references
Codex review: 4 turns (T2 caught the entity regression, T4 zero remaining findings)
Primary surfaces:
src/store.ts(tokenizeForFTS5,buildFTS5Query),src/entity.ts(gatherEntityFTSCandidates, bothentities_ftssites)
v0.10.5 — Issue #13: SQLite PRAGMA ordering race fix + openclaw doc-comment line-ref bump
v0.10.5 fixes yoloshii/ClawMem#13. One bug reported by @jcgau (first-time contributor):
src/store.ts:initializeDatabase() set PRAGMA busy_timeout = 15000 AFTER PRAGMA journal_mode = WAL. Because busy_timeout is a connection-level setting that only governs subsequent statements (default busy callback is NULL → SQLITE_BUSY returns immediately), the busy handler was not active when the first contending statement ran. When concurrent Stop-hook subprocesses (decision-extractor, handoff-generator, feedback-loop) opened the same SQLite file in parallel — both from OpenClaw's agent_end plugin-hook fan-out (src/openclaw/engine.ts:449) and from before_reset (src/openclaw/engine.ts:576), plus the Hermes MemoryProvider.on_session_end thread fan-out (src/hermes/__init__.py:518) — the first subprocess to acquire the journal-mode write lock succeeded and the rest returned SQLITE_BUSY immediately. Under the reporter's typical load (~48 heartbeat turns/day in their OpenClaw setup), two of three hooks failed per turn, silently dropping decision-extraction / handoff-generation / feedback-loop work for the losing subprocesses.
Vault on disk is byte-identical to v0.10.4. No schema migration. No env-var change. No public API change. Pure bun add -g clawmem upgrade.
The fix — one-line ordering swap, applied to two sites
src/store.ts:initializeDatabase() (writable init path) and src/store.ts:createStore() readonly branch both now set busy_timeout as the first statement on the connection, before sqliteVec.load(), PRAGMA journal_mode = WAL, and any DDL. The writable path uses 15000ms during DDL (well within the 30s Stop hook timeout); the terminal createStore() statement resets to operational 5000ms (or opts.busyTimeout) after DDL completes. The readonly branch is also hardened against the same race — public-API hardening, since no in-tree production caller currently passes readonly: true, but the ordering invariant should hold regardless.
The docstring in initializeDatabase() carries the rationale durably so a future refactor can't quietly re-introduce the race:
"busy_timeout is a connection-level setting that only governs subsequent statements (default busy handler is NULL → SQLITE_BUSY returns immediately), so it must precede the contending PRAGMAs. 15s is well within the 30s Stop hook timeout. createStore() resets to operational value (5000ms or opts.busyTimeout) after DDL completes."
Verification
The change set was validated against two turns of GPT-5.5 high-reasoning adversarial code review (cumulative ~401K tokens) under codex exec. Turn 1 verdict was APPROVED WITH MODIFICATIONS — zero High, one Medium (soften the readonly-branch comment from "takes a brief write lock" to "can contend when switching/initializing WAL state"), one Low (mention BOTH agent_end AND before_reset parallel Stop-hook fan-outs). Both modifications applied. Turn 2 cleared verbatim "Zero remaining findings on the Issue #13 fix. Ship as is. Ready to tag v0.10.5." Turn 2 also independently verified the skill-forge mirror byte-identical via cmp -s on all four changed files.
Test coverage: 3 new tests in tests/integration/store-concurrent-init.test.ts (NEW) + supporting tests/helpers/concurrent-init-worker.ts (NEW). Two source-text assertion gates (deterministic — catch the exact regression an accidental re-swap would introduce, anchored on the function body for initializeDatabase and on the // Readonly: comment marker for the readonly branch) plus one subprocess concurrent-init test (spawns 3 bun run worker processes against the same on-disk DB in mkdtempSync(tmpdir()), 60s timeout, asserts all 3 exit 0 without SQLITE_BUSY or "database is locked" in stderr).
The subprocess test mirrors the actual production scenario more faithfully than an in-process Promise.all could — bun:sqlite db.exec is synchronous, so in-process "concurrent" calls serialize on the JS event loop and do not contend on the SQLite file lock. :memory: stores have no file-system lock at all and cannot reproduce this bug — that's why the pre-existing tests/integration/store.test.ts (which uses :memory: exclusively) missed the regression.
Local run: bun test tests/integration/store-concurrent-init.test.ts reports 3 pass / 0 fail (18 expect() calls, 3.01s). bun test tests/integration/store.test.ts reports 33 pass / 0 fail (57 expect() calls, 496ms). No regressions.
Companion housekeeping — src/openclaw/index.ts doc-comment line-ref bump
Bundled with the v0.10.5 ship is a doc-only update to src/openclaw/index.ts carrying line-ref drift from the OpenClaw upstream-delta survey run on 2026-05-14 (upstream-delta-survey skill, main HEAD 5bb23c2f95 → 25eef1203a, 7091 commits in that window, non-breaking for ClawMem v0.10.x). Four doc-comment occurrences updated:
attempt.ts:2610→attempt.ts:2973—before_prompt_buildawait site (twice: backtick docstring at:41, inline comment at:164)attempt.ts:3379-3402→attempt.ts:3870-3892—agent_endfire-and-forget block (twice: backtick docstring at:40, inline comment at:178)
No runtime change. Standing "doc-line-refs ride the next release" pattern from prior cycles.
What didn't change
Retrieval pipeline, composite scoring, vault format, hook set, agent tool surface, OpenClaw plugin registration shape (
kind: memory), and Hermes plugin contract are all unchanged from v0.10.4.§14.3 contextEngine→memory migration block in
CLAUDE.md/AGENTS.mdpreserved verbatim.No public API change; no config change; no env-var change; no schema migration.
The terminal
PRAGMA busy_timeout = ${opts?.busyTimeout ?? 5000}at the bottom ofcreateStore()is preserved — for the writable branch it resets 15000 → 5000 after DDL; for the readonly branch it's a no-op rewrite to the same value (harmless, intentional simplicity).
Cross-references
Issue: https://github.com/yoloshii/ClawMem/issues/13 (@jcgau, first-time contributor)
Codex review: Turn 1 APPROVED WITH MODIFICATIONS, Turn 2 zero remaining findings
Parallel Stop-hook fan-out sites covered by this fix:
src/openclaw/engine.ts:449(handleAgentEnd),src/openclaw/engine.ts:576(handleBeforeReset),src/hermes/__init__.py:518(Python thread fan-out)Companion 2026-05-14 OpenClaw upstream-survey driving the doc-comment line-ref bump: a local memory note (
memory/openclaw-v2026.4.x-analysis.md)
v0.10.4 — Profile-aware setup openclaw + --help short-circuit (issue #11)
v0.10.4 fixes yoloshii/ClawMem#11. Two bugs reported by @elquercarlos:
clawmem setup openclawignoredOPENCLAW_STATE_DIRand OpenClaw's--profileflag. Pre-v0.10.4 hardcoded~/.openclaw/extensions/clawmemand never consulted env vars or OpenClaw's own destination-resolution logic. Users running OpenClaw with a non-default profile (e.g.~/.openclaw-dev) got the plugin installed in the wrong directory, where their active profile couldn't see it.clawmem setup openclaw --helpran setup instead of printing help. The handler had no argv short-circuit for--help/-h.
Both bugs close on this release. Vault on disk is byte-identical to v0.10.3. No schema changes, no env-var changes for default-profile users, no retrieval-pipeline or hook changes. Pure bun update -g clawmem upgrade.
cmdSetupOpenClaw — three-path install (§28.1)
The setup command now picks one of three paths at runtime:
Delegated copy mode (default, OpenClaw CLI on
PATH). Spawnsopenclaw plugins install <pluginDir> --force. OpenClaw owns destination resolution (which respectsOPENCLAW_STATE_DIR,OPENCLAW_CONFIG_PATH, and the--profileflag), runs manifest validation + security scans, persists install records, applies slot selection, and refreshes the registry. The plugin is auto-enabled by the install — the post-install "Next steps" output no longer printsopenclaw plugins enable clawmem.--forcemakes the install idempotent across re-runs (OpenClaw's default install mode rejects existing targets).Delegated link mode (
--linkflag, OpenClaw CLI onPATH). Spawnsopenclaw plugins install <pluginDir> -l, which records the source inplugins.load.paths— a load-path entry, not a filesystem symlink. Discovery uses the recorded load-path entry directly, so the v2026.4.11 symlink-discovery skip does NOT apply here. ClawMem does manual stale-install cleanup before delegating because OpenClaw rejects--forcewith--link.Direct-copy fallback (CLI absent). Falls back to recursive
cpSync(or filesystem symlink with--link) at a destination resolved by a faithful mirror of OpenClaw'sresolveConfigDir:OPENCLAW_STATE_DIR→OPENCLAW_CONFIG_PATH(config root =dirname(file)) →OPENCLAW_HOME/HOME/USERPROFILE/os.homedir()/cwd→~/.openclaw. The user gets a warning surfacing reduced capability (no manifest validation, no security scan, no install records). Filesystem symlink in the fallback's--linkpath is still subject to OpenClaw v2026.4.11+'s discovery skip — install OpenClaw to get the cleaner delegated behavior.
The faithful-mirror resolver matches OpenClaw exactly, including the asymmetry where OPENCLAW_STATE_DIR / OPENCLAW_CONFIG_PATH apply only .trim() (so OPENCLAW_STATE_DIR="undefined" is a literal directory name) while home-resolution env vars filter the literal strings "undefined" / "null" (matching OpenClaw's home-dir.ts:normalize). Diverging here would mean the delegated path and the fallback path install into different locations for the same env, which is exactly the bug class §28.1 set out to fix.
--remove — legacy-compatible uninstall
clawmem setup openclaw --remove now tries openclaw plugins uninstall clawmem --force first (when the CLI is available) and falls back to manual cleanup at the resolved extensions path. The fallback runs in two cases:
CLI uninstall fails (typically because the install was a legacy unmanaged direct-copy from pre-v0.10.4 ClawMem and isn't tracked in OpenClaw's plugin install records). On failure, ClawMem warns the user that OpenClaw config and install records may still need manual repair, then runs the manual cleanup. We do not silently mask managed-uninstall failures.
CLI uninstall succeeds (managed install). Even on success, ClawMem then checks the exact
extensions/clawmempath and removes any remaining symlink or directory — a "constrained stale cleanup" that handles the side-by-side case of a managed-link install plus a leftover unmanaged-copy directory from an earlier ClawMem version.
--help / -h short-circuit (§28.2)
cmdSetupOpenClaw short-circuits --help / -h at the top of the handler before any spawn or filesystem work and prints the full flag + env-var reference. Documents: --link (with separate behavior in delegated load-path mode vs filesystem-symlink fallback), --remove, env vars consulted (OPENCLAW_STATE_DIR, OPENCLAW_CONFIG_PATH, OPENCLAW_HOME, HOME, USERPROFILE), and example invocations including the headline OPENCLAW_STATE_DIR=~/.openclaw-dev clawmem setup openclaw.
Verification
The change set was validated against four turns of GPT-5.5 high-reasoning adversarial code review (cumulative ~292K tokens) under codex exec. All findings — three HIGH (delegated-install auto-enable messaging, idempotence-via---force, legacy-compatible --remove), two MEDIUM (resolver fidelity, real-stub-binary integration tests), two LOW (assertion tightening, main --help line) — were addressed before final clearance. The Turn 4 verdict was an explicit "zero remaining concerns, ready to ship v0.10.4."
Test coverage: 8 unit tests (tests/unit/openclaw-paths.test.ts) on the resolver helpers, including precedence (OPENCLAW_STATE_DIR over OPENCLAW_CONFIG_PATH), tilde expansion, OPENCLAW_HOME priority, os.homedir() failure → cwd fallback, and the asymmetric "undefined" / "null" literal handling that mirrors OpenClaw exactly. 6 integration tests (tests/integration/setup-openclaw.integration.test.ts) exercise the real subprocess boundary via a per-command shell stub on a sandboxed PATH: copy mode passes --force and not -l; link mode passes -l and not --force; install failure aborts (no silent fallback to direct copy); --remove with a managed install runs CLI uninstall AND constrained stale cleanup; --remove with a legacy install falls back to manual cleanup with the user-visible warning; CLI absent honors OPENCLAW_STATE_DIR in direct-copy mode. Two further integration tests prove the dual next-steps messaging differs between paths and that --help short-circuits before the openclaw --version probe.
What didn't change
Retrieval pipeline, composite scoring, vault format, hook set, agent tool surface, OpenClaw plugin registration shape (
kind: memory), and Hermes plugin contract are all unchanged.The §14.3 contextEngine→memory upgrade migration block is preserved verbatim.
Plugin source files (
src/openclaw/) are unchanged. The change is entirely incmdSetupOpenClawand a newsrc/openclaw-paths.tshelper module.Existing regression-gate tests in
tests/unit/openclaw-plugin.test.ts(84 source-text assertions) all still pass — the Path 3 fallback branch preserves the original next-steps output verbatim.
Cross-references
Issue: https://github.com/yoloshii/ClawMem/issues/11 (@elquercarlos)
BACKLOG:
BACKLOG.mdSource 28 — full scope including the codex-validated implementation planOpenClaw delegation surfaces:
openclaw/src/cli/plugins-install-command.ts:669(linked-path branch),openclaw/src/cli/plugins-install-persist.ts:182(auto-enable + slot selection),openclaw/src/utils.ts:119(resolveConfigDir)Helper module:
src/openclaw-paths.ts(mirrors OpenClaw's path-resolution semantics for the fallback path)
v0.10.3 — A-MEM parser hardening for noisy llama-server output (PR #7) + batched doc maintenance
v0.10.3 is a small patch release. The retrieval pipeline, composite scoring, vault format, hook set, agent tool surface, OpenClaw plugin registration shape, and Hermes plugin contract are all unchanged from v0.10.2. The release hardens the A-MEM JSON parser against a class of noisy llama-server outputs that v0.10.2 mishandled (parser silently picked an example/schema literal over the real payload, link-generation batches got zeroed when the LLM under-delivered), and bundles four sitting-in-tree doc updates that were waiting for the next release.
Vaults from v0.10.2 are byte-identical at rest. No schema migration. Default behavior is byte-identical for users whose llama-server outputs were already parsing cleanly — the parser only changes what it returns on inputs that previously fell through to repair paths or to zeroed link batches. Pure bun add -g clawmem upgrade.
A-MEM parser hardening (PR #7 by @cymkd / Veljko Simakovic)
The pre-PR parser had two real-world failure modes when the llama-server LLM emitted prompt-shaped prose alongside the real payload:
Prose-balanced literal won over the real payload. When the model echoed phrases like
Return empty array [] if no structured facts found.before the real fenced JSON answer, the parser locked onto the prose[]and returned it as the result. Conversation-synthesis runs that included the actual prompt wording in the assistant context exhibited this regularly. After the fix, the later real payload wins via a precedence order that walks all parseable balanced JSON candidates in source order, prefers payload-cued candidates (Actual:,Result:,Final answer:,Answer:), then avoids example-cued (example,e.g.,schema) and inline-prose literals, then falls back to the first candidate.parseJsonCandidatenow searches forward for a later line-start[/{when the first balanced candidate sits behind an example cue or at a non-line-start position with no payload cue.extractJsonFromLLMtightens its precedence so that outside-of-fences JSON before a preferredjsonfence requires a payload cue rather than winning by virtue of position.Link-generation under-delivery zeroed the batch. The pre-PR
generateMemoryLinksenforced an all-or-nothing completeness gate: if the LLM returned 4 valid links for a 5-neighbor prompt, or repeated atarget_idx, or referenced an out-of-range index, the entire batch was discarded and0links were created. After the fix, partial-valid insertion semantics are restored: a 5-neighbor prompt with 4 valid returned links inserts 4 rows. Duplicatetarget_idxentries are logged (Skipping duplicate link target N) and skipped after the first valid link for that neighbor. Out-of-range entries are logged (Skipping out-of-range link target N) and skipped instead of aborting already-valid links. The commit message includes the directive "Do not reintroduce all-or-nothing link generation without corpus measurements" to lock the contract.
Item-shape validation in parseLinkGenerationFromLLM (src/amem.ts:55,99) is unchanged and continues to reject malformed items strictly (missing fields, wrong types, non-finite confidence, bad relation type, non-positive/non-integer target_idx) before they reach the insert loop.
The PR went through three adversarial review rounds: Turn 1 surfaced 3 findings (1 HIGH on the prose-precedence behavior + 2 Medium on the under-delivery zeroing path and a sanitization edge), Turn 2 confirmed Findings 2-3 fixed and surfaced 1 HIGH on a regressed prose-fence ordering case + 1 MEDIUM on an un-flagged completeness gate that wasn't parser-level. Turn 3 verified both Turn 2 findings fixed (cymkd ran an independent GPT-5.5 high-reasoning pass before pushing the Turn 2 follow-up commit) and surfaced 2 LOW findings deferred to a future release (see below).
Two known LOW limitations deferred to v0.10.4+
Both LOWs were surfaced by the Turn 3 GPT-5.5 high-reasoning review pass and are explicit-acceptance candidates per the contributor's own "intentionally broad for this repro; can be tightened later" framing:
Outside-JSON-before-
json-fence precedence change. The newoutsidePrecedesPreferredJsonFencegate causes raw line-start JSON before a non-examplejsonfence to lose to the fence. Repro:[{"key":"real"}]\nfollowed by ajsonfence containing[]parses as[]. Real behavior change from "first raw JSON wins," but the affected pattern is narrow — well-formed raw payload immediately followed by a non-examplejsonfence is uncommon in observed llama-server output, and the broader fix the gate enables (the prose-before-fence repro above) is the more frequent failure mode.schemacue breadth.hasExampleCueBeforerecognizesschemaalongsideexampleande.g.to fix the reportedSchema: {...}\n[{...real...}]repro, but the substring match is broad enough to suppress real fenced payloads following phrases likeSchema validation result:. A tighterschema:/json schema:boundary regex would close the false-positive without losing the original repro coverage.
Both will be addressed in the next release with measurement-backed fixes — either tightening the heuristics with a corpus-measurement pass over real llama-server output, or accepting them with regression tests locking in the new contract. That decision belongs with the data, not this PR.
Batched doc maintenance (rides this release per option-(a) standing direction)
Four user-facing doc updates that accumulated in the working tree across the v0.10.2 → v0.10.3 window, all from in-session OpenClaw delta surveys:
src/openclaw/index.tsline-ref drift (cosmetic, no behavior change). OpenClaw advanced from v2026.4.21 to HEAD1f724bc50b(2026-05-04, post-v2026.4.26 untagged) across two consecutive surveys.attempt.tswas reorganized undersrc/agents/pi-embedded-runner/run/as part of a runner refactor; line refs in our doc-comment correctness contracts shifted twice:before_prompt_buildawait contextattempt.ts:1873→:2294→:2610;agent_endfire-and-forget blockattempt.ts:2470-2496→:3023-3048→:3379-3402. Both await/fire-and-forget contracts hold across both bumps; only line numbers shifted. 4 occurrences updated in the file header docstring and thebefore_prompt_build/agent_endhandler comments.README.md,CLAUDE.md,AGENTS.md,SKILL.md— 30sagent_endvoid-hook timeout disclosure. OpenClaw v2026.4.26 (commit4d4c7c8ab3) introducedDEFAULT_VOID_HOOK_TIMEOUT_MS_BY_HOOK = { agent_end: 30_000 }insrc/plugins/hooks.ts. A timed-out handler is logged ("timed out after 30000ms") and the runner continues, but the plugin's underlying work is not cancelled. ClawMem'sagent_endruns decision-extractor + handoff-generator + feedback-loop; warm-cache postrun is well under 30s, but cold-start indexing or LLM stalls could approach this. The disclosure is operational, not behavioral — it doesn't change what ClawMem does, only adds a log warning above the 30s threshold for users with cold-start scenarios that exceed it. Single-sentence insertion in each doc, adapted to surrounding tone, fail-open framing emphasized. CLAUDE.md and AGENTS.md remain byte-identical post-edit.
Per the option-(a) standing direction these doc updates do not justify a release on their own; they ride PR #7 which is the next real-functionality release.
External credit
Veljko Simakovic / @cymkd — opened yoloshii/ClawMem#7 with the four-commit progressive parser hardening (initial parse robustness, object-wrapped result handling, prose-vs-payload preservation, and the Turn 2 follow-up that fixed the regressed prose-fence ordering and removed the un-flagged completeness gate). The PR went through three adversarial review rounds (gpt-5.4 high reasoning Turns 1-2, gpt-5.5 high reasoning Turn 3); the contributor independently ran a gpt-5.5 high-reasoning pass on his own diff before pushing the Turn 2 follow-up, which surfaced and fixed several edge cases before they hit our review queue. Cross-validated against
tests/unit/amem.test.ts+tests/unit/conversation-synthesis.test.ts+tests/integration/conversation-synthesis-two-pass.integration.test.ts(107 pass / 0 fail / 220 expect() calls, was 102 pre-PR; +5 new regression tests). Two Turn 3 LOW findings were explicitly deferred to the next release rather than chasing a fifth round, in line with cymkd's own forward-looking framing on the schema cue breadth.
Test coverage
PR #7 adds five new regression tests in tests/unit/amem.test.ts covering: prose [] before a fenced real payload, the actual conversation-synthesis prompt wording echoed before a fenced payload, prose schema object before a later real raw array, parseLinkGenerationFromLLM parsing the later real link array instead of the schema object, and generateMemoryLinks inserting a valid partial batch while skipping duplicate/out-of-range targets.
Targeted suite: 107 pass / 0 fail / 220 expect() calls across the three test files (was 102 pre-PR; +5 new tests).
v0.10.2 — Configurable remote LLM endpoints + doc maintenance
v0.10.2 is a small patch release. The retrieval pipeline, composite scoring, vault format, hook set, agent tool surface, and OpenClaw plugin registration shape are all unchanged from v0.10.0/v0.10.1. The release adds three opt-in env vars for users running ClawMem against OpenAI-compatible remote LLM proxies, fixes a /v1 URL-doubling edge case in the remote LLM transport, and applies two small doc updates that were sitting in the working tree.
Vaults from v0.10.1 are byte-identical at rest. No schema migration. Default behavior is byte-identical for users with no new env vars set — same hard-coded qwen3 model, same /no_think suffix, same request body field order, same http://localhost:8089/v1/chat/completions URL. Pure git pull upgrade.
Configurable remote LLM endpoints (PR #8 by @DrJsPBs)
The remote LLM transport in src/llm.ts previously hard-coded model: "qwen3" and the /no_think prompt suffix in the /v1/chat/completions request body. That worked for the QMD native combo (qmd-query-expansion-1.7B on :8089 via llama-server) but blocked anyone wanting to point ClawMem at an OpenAI-compatible proxy with a different model name or at a non-Qwen endpoint that treats /no_think as literal prompt text. v0.10.2 adds three opt-in env vars to make all three knobs user-configurable while preserving the existing defaults exactly:
| Env var | Default | Effect |
|---|---|---|
CLAWMEM_LLM_MODEL | qwen3 | Model name sent in the request body. Override for OpenAI-compatible proxies (e.g. gpt-5.4-mini). |
CLAWMEM_LLM_REASONING_EFFORT | (unset, field omitted) | Optional top-level reasoning_effort field for Chat Completions endpoints that support it. Validated against the enum none / minimal / low / medium / high / xhigh; unsupported values log a warning and are ignored. Leave unset for llama-server / vLLM unless your serving stack explicitly accepts the field. |
CLAWMEM_LLM_NO_THINK | true | Append /no_think to remote prompts. Set false for standard OpenAI models and other endpoints that reject or treat the Qwen-style suffix as literal prompt text. |
The settings are threaded through both plugin layers:
OpenClaw plugin —
src/openclaw/openclaw.plugin.jsonexposesgpuLlmModel,gpuLlmReasoningEffort(with the same enum constraint), andgpuLlmNoThinkconfig keys.src/openclaw/index.tsmaps each to the correspondingCLAWMEM_LLM_*env var only when the user explicitly set it (so unset config falls through to the runtime default rather than overriding it with an empty string).Hermes plugin —
src/hermes/__init__.pyadds the three keys to itsget_config_schema()(allsecret: False) and to the env-passthrough tuple ininitialize(). The module docstring lists them alongside the existingCLAWMEM_LLM_URL.
/v1 URL doubling fix
The pre-PR transport posted to ${remoteLlmUrl}/v1/chat/completions unconditionally. If a user set CLAWMEM_LLM_URL=https://api.example.com/v1 (a common shape for OpenAI-compatible proxies that document their base URL with /v1 already included), the request went to https://api.example.com/v1/v1/chat/completions and 404'd. v0.10.2 introduces buildRemoteChatCompletionsUrl() which strips trailing slashes, detects an existing /v1 suffix, and only appends /chat/completions in that case. All four shapes now resolve correctly: http://host, http://host/, http://host/v1, http://host/v1/.
Validation centralization
normalizeRemoteLlmReasoningEffort() is now the single normalization point for the reasoning-effort value. Previously the env-bootstrap path did its own .trim().toLowerCase() + Set check and the constructor did nothing — so a direct caller passing LlamaCpp({remoteLlmReasoningEffort: " HIGH "}) would have posted "reasoning_effort":" HIGH " to the endpoint while the env path correctly normalized it to "high". The constructor now calls the helper for both paths, so env and direct config behave identically. CLAWMEM_LLM_MODEL is also trimmed at both surfaces (defense-in-depth — whitespace-padded values from .env-style configs no longer post "model":" gpt-5.4-mini ").
Doc maintenance
Two small doc changes that were sitting in the working tree from the v2026.4.18 / v2026.4.16-920 changelog surveys ride this release:
docs/guides/hermes-plugin.md— preventive warning added to the Install section: do NOT addclawmemtoplugins.enabledin~/.hermes/config.yaml. Hermes #11xxx onwards made all general plugins opt-in by default;plugins.enabledis the general-plugin opt-in roster, not the memory-provider activation channel. Memory providers are activated viamemory.provider: clawmem, completely separate from the general plugin loader. Addingclawmemtoplugins.enabledwould cause the general loader to import it as akind: standaloneplugin and callregister(ctx)against the generalPluginContext— which doesn't exposeregister_memory_provider, so the import errors and a warning gets logged. Harmless but noisy. The warning heads off the easy mistake for users reading the install path docs literally and conflating the two settings.src/openclaw/index.ts— cosmetic line-ref drift fix in the file-header docstring and correctness-contract comments. OpenClaw'sbefore_prompt_buildis still awaited atattempt.ts:1873(was:1642at v2026.4.18 cutoff);agent_endis still fire-and-forget atattempt.ts:2470-2496(was:2198-2224). No semantic change — the await/fire-and-forget contracts hold; only the line numbers in our reference comments shifted.
External credit
@DrJsPBs / @DrJLabs — opened yoloshii/ClawMem#8 with the configurable remote LLM env vars, the OpenClaw + Hermes plumbing, the
/v1URL doubling fix, the validation centralization, and 5 new contract tests covering whitespace handling, URL normalization, env-vs-direct config consistency, and the byte-identical default-preservation contract. The PR went through two adversarial review rounds (gpt-5.4 high reasoning each side); the contributor independently identified and folded in fork-review extras (the/v1fix being the standout) within the same PR scope. Cross-validated in their downstreamDrJLabs/ClawMemfork before opening upstream — exactly the integration discipline the v0.7.x community-contributor track has been rewarding.
Test coverage
PR #8 adds three new test files:
tests/unit/llm-remote-config.test.ts(new) — the contract tests for the new env vars and the/v1URL buildertests/unit/hermes-plugin.test.ts(new) — covers the schema additionstests/unit/openclaw-plugin.test.tsextended — covers the env-mapping consistency
Targeted suite: 97 pass / 0 fail / 171 expect() calls across the three files (was 92 pre-PR; +5 new tests).
v0.10.1 — Hermes agent_context isolation + OpenClaw v2026.4.18 / Hermes v2026.4.16+ doc maintenance
v0.10.1 is a small patch release. The retrieval pipeline, composite scoring, vault format, hook set, agent tool surface, and OpenClaw plugin registration shape are all unchanged from v0.10.0. The release covers two upstream changelog reviews against the OpenClaw and Hermes runtimes ClawMem integrates with — neither produced any breaking changes — plus one correctness fix in the Hermes plugin and a small set of doc updates that keep the public-facing surfaces aligned with the runtimes users are actually running.
Vaults from v0.10.0 are byte-identical at rest. No schema migration, no new env vars, no new dependencies. Pure git pull upgrade for Claude Code users; clawmem setup openclaw re-run is optional (no plugin code changes that affect runtime behavior); Hermes users on a non-primary agent_context (subagent, cron, flush) get a quiet correctness improvement.
Hermes Agent — agent_context isolation in src/hermes/__init__.py
Hermes's MemoryProvider ABC docstring is explicit: "Providers should skip writes for non-primary contexts (cron system prompts would corrupt user representations)." Hermes's run_agent.py passes an agent_context kwarg to every MemoryProvider.initialize() call with one of "primary", "subagent", "cron", or "flush". Pre-v0.10.1 the ClawMem plugin absorbed the kwarg via **kwargs and ran the full lifecycle for every context — including writing transcript turns and running decision-extractor / handoff-generator / feedback-loop / precompact-extract on cron and subagent passes. The vault's saveMemory dedup limited the blast radius, but the cleaner answer is the one the ABC asks for.
v0.10.1 honours the contract. The plugin now reads agent_context in initialize() and gates only the write-side surfaces; the read-side surfaces still run for every context so non-primary agents continue to benefit from retrieval:
| Surface | Direction | agent_context != "primary" |
|---|---|---|
session-bootstrap (in initialize) | Read | Runs |
prefetch() / queue_prefetch() (context-surfacing) | Read | Runs |
system_prompt_block() | Read | Runs |
Agent tools (REST: clawmem_retrieve etc.) | Read | Runs |
sync_turn() (transcript append) | Write | Suppressed |
on_session_end() (extraction trio) | Write | Suppressed |
on_pre_compress() (precompact-extract) | Write | Suppressed |
When the active context is non-primary, initialize() logs a single info-level line for operator visibility:
clawmem: agent_context=cron — reads enabled, writes suppressedNet effect: subagents and cron agents read the vault as before; they no longer write back into it. For installs that run only agent_context="primary" (the default for interactive CLI / Telegram / Discord platforms), behavior is unchanged.
Hermes Agent — preferred install path now $HERMES_HOME/plugins/clawmem/
Hermes #10529 (in v2026.4.13+) added user-plugin discovery: plugins/memory/__init__.py now scans $HERMES_HOME/plugins/<name>/ in addition to the bundled hermes-agent/plugins/memory/<name>/. Both paths still work; the user-plugin path is preferred because it survives git pull of hermes-agent and avoids the dual-registration trap that previously caused duplicate tool names with strict providers. Discovery heuristic = grep __init__.py for register_memory_provider or MemoryProvider; both already present in src/hermes/__init__.py. README.md Install + Setup blocks, docs/guides/hermes-plugin.md, CLAUDE.md, AGENTS.md, and SKILL.md now lead with the user-plugin path and document the bundled path as a still-supported alternative.
OpenClaw v2026.4.18 — three relevant changes (all compatible)
A v2026.4.11 → v2026.4.18 changelog survey (1,794 commits) ran against ClawMem v0.10.0's plugin. Verdict: no breaking changes. Three changes touch surfaces ClawMem consumes; all are documented in this release:
Synchronous
register()enforcement (2a283e87a7, inv2026.4.19-beta.1). OpenClaw now throws"plugin register must be synchronous"if a plugin'sregister()returns a Promise. ClawMem'sregister(api)insrc/openclaw/index.tsis and always has been synchronous (noasync, no top-levelawait, returns void) — everyawaitlives inside per-event handlers, never in registration itself. Companion change: register failures now atomically roll back side effects, so any future throw insideregister()will leave OpenClaw in a clean state. The constraint is now documented as a load-bearing invariant inCLAUDE.md/AGENTS.md.memory-coredreaming sidecar coexistence (5fde14b844, #65411, in v2026.4.18). When ClawMem owns thememoryslot ANDplugins.entries.memory-core.config.dreaming.enabled = true, OpenClaw now loadsmemory-core's dreaming engine alongside ClawMem instead of unloading it entirely. Two valid configurations: ClawMem-only (default afteropenclaw plugins enable clawmem, withdreaming.enabled = false) or ClawMem + dreaming sidecar (opt-in, setdreaming.enabled = true). Documented indocs/guides/openclaw-plugin.md(new "Coexistence with memory-core dreaming sidecar" section),README.md,CLAUDE.md,AGENTS.md,SKILL.md.attempt.tsline drift. OpenClaw'sbefore_prompt_buildis still awaited;agent_endis still fire-and-forget. Their line numbers shifted between v2026.4.11 and v2026.4.18 (:1661 → :1642and:2226-2249 → :2198-2224). The four references insrc/openclaw/index.ts(file docstring + correctness-contract comments) were updated accordingly. No semantic change.
on_memory_write deliberately not opted into
Hermes #10507 added an on_memory_write bridge to the sequential tool execution path; the commit message names ClawMem as a beneficiary. v0.10.1 deliberately stays opted out. ClawMem's filesystem watcher already indexes Hermes's MEMORY.md / USER.md if those files live under a configured collection — layering an on_memory_write shell-out on top duplicates filesystem watching and introduces a remove-semantics mismatch (a hook event saying "this entry was removed" cannot be cleanly translated into ClawMem's content-addressed vault). The docs/guides/hermes-plugin.md lifecycle table now documents this rationale instead of the prior "future" framing.
v0.8.4 retroactive credit
### External credit subsection added to v0.8.4 in this file naming @saschabuehrle (Lemony.ai founder) for PR #6 (fix/issue-5). The PR was closed-as-superseded on 2026-04-11 because v0.8.4 ended up shipping the broader auto-install fix, but the gateway-restart-before-slot-assignment insight is preserved verbatim in v0.8.4's printed next-steps and the code comment that documents the constraint. Matches the v0.10.0 ### External credit precedent set for @withx. The credit landed in the working tree before the v0.10.1 cut and rides this release.
Test coverage
No new tests in v0.10.1 — the agent_context guard's behavior is observable only against a live Hermes runtime, and the rest of the release is documentation. Full v0.10.0 baseline holds: 1204 pass / 0 fail, 2339 expect() calls across 66 test files.
v0.10.0 — OpenClaw Pure-Memory Migration (§14.3) + v2026.4.11 Packaging Fix
v0.10.0 is the OpenClaw pure-memory migration. ClawMem's OpenClaw plugin is no longer a kind: context-engine plugin wrapping a ClawMemContextEngine class. It is a kind: memory plugin that wires every lifecycle event (before_prompt_build, agent_end, before_compaction, session_start) through OpenClaw's plugin-hook bus directly. This matches the direction OpenClaw's own context-engine slot has taken since v2026.4.x (narrowed to runtime compaction) and moves ClawMem to the slot it was always logically targeting: the exclusive memory slot, alongside memory-core and memory-lancedb (which it automatically displaces when enabled).
Why this matters — dual plugin surfaces across OpenClaw and Hermes
Over the last year, the OpenClaw and Hermes maintainers independently converged on the same architectural split: agent runtimes expose two distinct plugin surfaces, one for memory (persistent, cross-session, retrieval-first) and one for context engines (in-session, lossless or lossy compression, compaction-first). Hermes shipped its MemoryProvider ABC from the start, and ClawMem has always been plugged in there correctly — as a memory provider. OpenClaw's plugin kind vocabulary took longer to stabilize, with context-engine originally broad enough to accommodate both roles, then narrowing through v2026.4.x to what it is today: runtime compaction / compression specifically.
Under the stabilized definition, ClawMem is a memory layer, not a context engine. A memory layer maintains a durable index of prior sessions, decisions, and knowledge and serves them back into new conversations via retrieval. A context engine reshapes the live session window: compressing old turns, summarizing transcripts, handing off between models. These are different jobs on different time axes. Calling ClawMem a "context engine" was accurate for the ClawMem-as-OpenClaw-plugin wiring pre-v0.10.0 only because no other slot existed yet. Post-v0.10.0 it is misleading in both directions: it mislabels what ClawMem does, and it blocks OpenClaw users from pairing ClawMem with a genuine context-engine plugin (e.g. an LCM-compression plugin like lossless-claw) because the two would fight over the same slot.
On v0.10.0, OpenClaw users get the same composability Hermes users have had all along:
Memory slot:
clawmem— cross-session retrieval, knowledge graph, decision extraction, the 5 agent toolsContext-engine slot: free for an LCM/compression plugin that runs inside the live session window (e.g.
lossless-claw)
Both can be enabled at the same time. They do not overlap. The memory slot is about what you knew yesterday; the context-engine slot is about what fits in today's prompt. This is the end state the OpenClaw maintainers have been steering toward for several releases, and v0.10.0 is ClawMem moving to the seat that was prepared for it. For Hermes users nothing changes — ClawMem has always been correctly integrated as a memory provider there.
What is unchanged
The retrieval pipeline, composite scoring, profiles, vault format, hook set, <vault-context> output, and the 5 registered agent tools are unchanged. No schema migration, no new env vars, no new dependencies. The vault on disk is byte-identical to v0.9.0. Claude Code users who do not run OpenClaw can upgrade with no action beyond git pull.
v0.10.0 also fixes a packaging bug that surfaced when OpenClaw shipped v2026.4.11. The new discovery path (readdirSync({ withFileTypes: true }) + dirent.isDirectory()) started silently skipping ClawMem's symlinked plugin directory. Diagnosis, confirmation via an external user on yoloshii/ClawMem#5, and the complete fix (discovery manifest + copy-not-symlink install) are all in this release.
§14.3 — Pure-memory plugin registration
Plugin kind changes from
context-enginetomemory. The adapter insrc/openclaw/registers askind: memory. OpenClaw's exclusivememoryslot now holdsclawmem(viaopenclaw plugins enable clawmem, which also disables any competing memory plugin in the same step). The olderplugins.slots.contextEngine: "clawmem"pattern no longer applies — v0.10.0 does not occupy that slot.ClawMemContextEngineclass removed. Every lifecycle surface the plugin needs — prompt injection, post-turn extraction, pre-compaction state capture, session bootstrap — is now a plainPluginHookNamehandler on the plugin-hook bus.before_prompt_buildis the load-bearing path: it runs prompt-aware retrieval (context-surfacing every turn + cached bootstrap context on first turn) AND runsprecompact-extractsynchronously when token usage approaches the compaction threshold, so state is captured before the LLM call that could trigger compaction on this turn (no race with the compactor).agent_endruns decision-extractor + handoff-generator + feedback-loop in parallel (fire-and-forget at OpenClaw's call site).before_compactionis a defense-in-depth fallback only — fire-and-forget, races the compactor, exists solely to catch the rare case wherebefore_prompt_build's proximity heuristic missed a sudden token-count jump; it forces the precompact regardless of proximity since by the time it runs compaction is already in motion.session_startregisters the session and caches the bootstrap context for first-turn injection. This is a strict improvement over v0.3.0's shape, where the pre-emptive extraction happened insideContextEngine.compact()viadelegateCompactionToRuntime()— the v0.10.0 wiring moves the extraction up the stack intobefore_prompt_buildwhere it has a real pre-LLM hook to await on.Behavior is identical from the agent's point of view. The
<vault-context>block is byte-equivalent to v0.9.0 on the same vault with the same prompt. This is a packaging and registration change, not a behavioral one. The pure-memory shape is the architecturally correct home for what ClawMem has always done — the oldcontext-engineshape was load-bearing on an OpenClaw version that narrowed out from under us.
v2026.4.11 packaging fix (external report → yoloshii/ClawMem#5)
The packaging fix is co-resident with §14.3 because they are both in the same file and both required for v0.10.0 to run on the current OpenClaw release. The symptom was reported externally by @withx on a fresh install of OpenClaw v2026.4.11 + ClawMem — the plugin looked installed to every CLI command but never registered at runtime.
src/openclaw/package.jsonis the new discovery manifest. OpenClaw v2026.4.11'sdiscoverInDirectoryreadspackage.jsonand checks for theopenclaw.extensions: ["./index.ts"]field before descending into a candidate plugin directory. Pre-v0.10.0 ClawMem shippedopenclaw.plugin.jsonas the only manifest. That file is still shipped and still parsed at runtime, but it is not enough on its own for discovery on v2026.4.11+ — the plugin directory is silently skipped. v0.10.0 addspackage.jsonto the plugin source tree andclawmem setup openclawnow verifies it is present before copying.clawmem setup openclawdefaults to recursive copy, not symlink. OpenClaw v2026.4.11's discoverer walks the extensions directory withreaddirSync({ withFileTypes: true })and usesdirent.isDirectory()to decide which entries to descend into. Symlinks to directories reportisDirectory() === falseon that API shape, so a symlinked plugin is silently skipped during discovery and never registers. Every ClawMem release since the OpenClaw plugin was introduced shipped a symlinked install — it worked on OpenClaw v2026.3.x but stopped working on v2026.4.11.cmdSetupOpenClawnow runscpSync(..., { recursive: true, dereference: true })to install the plugin as a real directory. A new--linkopt-in flag preserves the old symlink behavior for local dev workflows and for older OpenClaw versions, with a warning that v2026.4.11+ discovery will skip the symlink.Next-steps output uses
openclaw plugins enable clawmem. The setup command now printsopenclaw plugins enable clawmeminstead ofopenclaw config set plugins.slots.memory clawmem. Theenableverb pre-validates that the plugin is in the discovered registry (so it only runs on a successful copy), switches the exclusivememoryslot, and disables the previous occupant (memory-core,memory-lancedb) in a single command. The olderconfig setpattern failed silently on v2026.4.11 because the slot validator rejected a plugin id that had not been discovered first.Multi-user ownership gotcha is documented. OpenClaw v2026.4.11 enforces that plugin directories be owned by the current runtime user or root, rejecting foreign-owned directories with
suspicious ownership (uid=X, expected uid=Y or root). This is a security feature (it prevents a gateway running as a privileged system user from loading code a less-privileged user dropped into its extensions directory). On single-user installs where the gateway runs as your own user account, the ownership check passes automatically. On deployments where the gateway runs as a dedicated system user (e.g.openclaw) different from the installer user (e.g.deploy-user), you mustsudo chown -R <gateway-user>:<gateway-group> ~/.openclaw/extensions/clawmemafter running setup. Documented indocs/guides/openclaw-plugin.mdInstall section anddocs/troubleshooting.mdOpenClaw section.
Test coverage
+2 regression gates on top of the v0.9.0 test baseline, locking in the v2026.4.11 packaging fix:
tests/unit/openclaw-plugin.test.ts::cmdSetupOpenClaw defaults to copy mode (v2026.4.11+ compat)— asserts thecpSync(pluginDir, linkPath, ...)call exists incmdSetupOpenClawand the--linkopt-in is parsed viaargs.includes("--link"). If a future edit flips the default back to symlink without also updating the assertion, the test fails loudly.tests/unit/openclaw-plugin.test.ts::src/openclaw/package.json declares openclaw.extensions (v2026.4.11+ discovery gate)— readssrc/openclaw/package.json, assertstype === "module"andopenclaw.extensionsis an array containing"./index.ts". If the manifest is ever deleted or reshaped without updating this test, the suite fails before the change reaches release.
Public test suite: 1105 → 1107, zero regressions. The one pre-existing assertion in the Shipping Condition 2 — setup-time migration text is present suite was updated from "plugins.slots.memory clawmem" to "openclaw plugins enable clawmem" to match the new next-steps output shape.
Multi-user end-to-end validation
Captured on a representative multi-user install (OpenClaw gateway runs as system user openclaw, ClawMem installed by a separate admin user):
[plugins] clawmem: plugin registered (kind=memory, bin=/home/<user>/clawmem/bin/clawmem, profile=balanced, budget=800)
[plugins] clawmem: registered 5 agent tools
[gateway] ready (7 plugins: acpx, browser, clawmem, device-pair, phone-control, talk-voice, telegram; 11.3s)The runtime registration log line explicitly emits kind=memory, which is the §14.3 change proving the new registration path is live. The clawmem entry appears in the gateway ready line alongside the stock plugins, proving the packaging fix clears v2026.4.11's discovery gate. The multi-user ownership check (suspicious ownership (uid=1001, expected uid=997 or root)) reproduced and cleared after sudo chown -R openclaw:openclaw ~/.openclaw/extensions/clawmem, which is now a documented step in docs/guides/openclaw-plugin.md for multi-user deployments.
Codex review
GPT 5.4 High, session 019d72d5 (continues the session chain used from v0.7.1 through v0.9.0, now cumulative across 20+ turns). The §14.3 implementation reached zero remaining findings before the v2026.4.11 packaging gap was discovered. Codex was re-engaged with the packaging compat delta (new package.json, copy-default cmdSetupOpenClaw, next-steps rewrite, +2 regression gates, multi-user e2e evidence) for a final pre-ship pass, same session.
External credit
@withx — reported the OpenClaw v2026.4.11 incompatibility on yoloshii/ClawMem#5 and confirmed the runtime symptom on a fresh install that did not share any state with the development machines. The external reproduction is what made it clear this was a clean packaging gap, not a machine-specific config drift.
Upgrading from v0.9.0
v0.10.0 is drop-in safe for Claude Code users: cd ~/clawmem && git pull && systemctl --user restart clawmem-watcher.service. Nothing on the retrieval path changed.
OpenClaw users must also re-run clawmem setup openclaw (to switch the extensions dir from symlink to recursive copy), chown the new directory to the gateway user on multi-user installs, and restart the gateway. The full step-by-step is in docs/guides/upgrading.md. Upgrade OpenClaw to v2026.4.11+ first — v0.10.0's setup and discovery behavior depend on v2026.4.11's new plugin discovery contract.
v0.9.0 — <vault-facts> KG Injection + Session-Scoped Focus Topic Boost
Two context-surfacing upgrades. §11.1 adds a <vault-facts> SPO-triple injection block inside <vault-context> so the model gets structured "what is currently true about the entities in this prompt" alongside the existing <facts> (surfaced documents) and <relationships> (memory-graph edges) blocks. §11.4 adds a per-session topic steering lever (clawmem focus set "<topic>" --session-id <id>) that biases retrieval toward a declared topic for the duration of a working session without mutating any persisted state. Both changes live exclusively on the read path and are fail-open — the baseline pre-v0.9.0 <vault-context> shape is byte-identical when the new stages don't fire, and every downstream stage (threshold, diversification, token budget, injection) is unchanged. v0.8.5 was the last feature release; v0.9.0 is drop-in safe: one idempotent expression-index migration, no breaking API changes, no required reindex/embed, no schema rewrites.
§11.1 — <vault-facts> knowledge-graph injection
Three-path prompt-only entity extraction — the seed set for
<vault-facts>comes from the raw prompt ONLY, via three independent paths: (a) a canonicalvault:type:slugregex (e.g.default:project:clawmem), (b) proper-noun extraction validated via exact-matchresolveEntityTypeExactthat skips ambiguous names resolving to multiple types, and (c) a longer-first n-gram scan (3-gram > 2-gram > 1-gram, cased + lowercased) for technical vocabulary likevector-store,oauth2,gpu node. Prompt-only is a HARD CONSTRAINT — entity seeds never come fromsurfacedDocs[i].bodyor any retrieval-phase field, so a topic-boosted off-topic doc from §11.4 cannot pollute the facts block with facts about unrelated entities.Validate-then-count candidate ordering (Codex §11.1 Turn 5 invariant) — per-path, candidates are validated against the entity index BEFORE counting against the 100-candidate cap. Without this ordering, a long prompt dominated by unvalidated capitalized noise (path b raw extraction) would starve the lowercase/hyphenated n-gram lane (path c) and the technical-vocabulary recall would silently drop. The cap also preserves prompt order as a tiebreaker so the first mentioned entities win under pressure.
Cross-path entity-id dedup + longer-n-gram tie-breaker — if the same entity resolves via multiple paths (e.g. "ClawMem" as a proper noun AND "clawmem" as a 1-gram), the cross-path dedup collapses it to one
entity_idand the sourcePath from the first-matching path wins. For n-gram ties, longer n-grams outrank shorter ones (vector-storeas a 1-gram compound beatsvector+storeas separate tokens).Cross-entity triple dedup (Codex §11.1 Turn 1 fix) — when both endpoints of a triple are seeded from the prompt (e.g. the prompt mentions both
ClawMemandBun, and the graph hasClawMem depends_on Bun),store.queryEntityTriplesreturns the triple from both sides (outgoing from ClawMem, incoming to Bun).buildVaultFactsBlocknow dedupes by a stable${subject}\u0000${predicate}\u0000${object}key before budgeting so the same fact isn't emitted twice and budget isn't spent twice. Per-entitymaxTriplesPerEntitycap still applies BEFORE dedup (anti-monopoly is enforced per entity first, then dedup collapses cross-entity duplicates).Profile-gated
factsTokenssub-budget — a newProfileConfig.factsTokensfield gives<vault-facts>a dedicated token allowance that cannot steal from the existing<facts>/<relationships>budget.speed=0 disables the stage entirely (zero overhead on the fast profile).balanced=200 tokens (~50 triples at default 4-char/token estimate).deep=250 tokens. Truncation always at the triple boundary, never mid-triple, never emits an empty block. IfOVERHEAD >= budgetTokensthe block is dropped — established blocks take priority.Schema migration —
idx_entity_nodes_lower_name—store.tsaddsCREATE INDEX IF NOT EXISTS idx_entity_nodes_lower_name ON entity_nodes(LOWER(name), vault)on first open. This expression index backs the newbatchLookupNamesquery (WHERE LOWER(name) IN (...) AND vault = ?) which would otherwise degenerate to a full scan on large vaults. Idempotent, runs once, harmless if the binary is later rolled back to v0.8.5 (SQLite ignores the unused index).Fail-open at every stage — empty entity set → skip the stage (caller gets unchanged
vaultInner). Per-entityqueryTriplesthrow → skip that entity and continue. Budget too small for even one triple → drop the whole block. Any exception inside theif (profile.factsTokens > 0) { try { ... } catch {} }wrapper → degraded vault behaves identically to pre-§11.1. The baseline<vault-context>is always recoverable.
§11.4 — Session-scoped focus topic boost
clawmem focus set / show / clearCLI — three new subcommands write/read/delete a per-session focus file at~/.cache/clawmem/sessions/<session_id>.focus(1024-byte cap, UTF-8, plain text). Session ID is resolved from--session-id <id>, thenCLAUDE_SESSION_ID(Claude Code exposes this natively), thenCLAWMEM_SESSION_ID. The focus file IS the primary signal read bycontext-surfacing—CLAWMEM_SESSION_FOCUSenv var is a debug-only override that is NOT session-scoped and should not be used in multi-session deployments.CLAWMEM_FOCUS_ROOTenv override is supported for hermetic testing.Intent-threaded retrieval — when a focus topic is resolved,
context-surfacingthreads it as theintentparameter toexpandQuery+rerank+extractSnippet. This is the existing query-time intent lever (already Codex-approved in the query pipeline), so the topic steers query expansion variants, reranker priority, and snippet-extraction sentence selection along the same code paths as a manually-providedintenton an MCPquery()call.Post-composite-score topic boost — after
applyCompositeScoringand BEFORE the adaptive threshold filter + memory-type diversification stages, docs matching ALL tokens of the focus topic (bag-of-words against title/path/body[:800], case-insensitive) get a 1.4× multiplier oncompositeScore. Non-matching docs get a 0.75× demote (clamped at a 0.5 floor viaMath.max(demoteFactor, 0.5)). The boost fires per-result and re-sorts the scored set, so matching docs rise and non-matching docs drop but stay eligible if they would have made the threshold without the demote.Zero-match NO-OP fail-open contract (Codex §11.4 Turn 1 fix) — if zero docs in the scored result set match the topic,
applyTopicBoost(...)early-returns without mutatingcompositeScore. Without this short-circuit, uniformly demoting every non-matching doc would push borderline docs below the downstream adaptive threshold and silently shrink the result set compared to the no-topic baseline — a direct regression against the approved §11.4 spec ("topic set + zero matching docs → proceed with the normal results"). The fix pre-computes a per-result match flag array in a single pass, checksif (!anyMatch) return scored, and only enters the mutation loop when at least one match exists.Session isolation — no SQLite writes — the focus file is scoped by
sessionId, never writes to SQLite, never mutatesconfidence/status/snoozed_until/archived_at/ any lifecycle column. Concurrent sessions on the same host cannot cross-contaminate each other's topic biasing. This is load-bearing for multi-session deployments (OpenClaw daemon, mission-control concurrent chats) — a globalmemory_snoozewould not have worked for session-scoped noise suppression without polluting other sessions' retrieval state.Hook-level integration test lock-in — three new tests in
tests/integration/context-surfacing-topic-boost.integration.test.tsdrive the realcontextSurfacing(store, input)handler end-to-end against a hermetic in-memory SQLite store with redirectedCLAWMEM_FOCUS_ROOT: (1) no-topic vs non-matching-topic produces byte-identical<vault-context>output, (2) the byte-equality invariant holds even on a 3-doc result set exercising the full threshold + diversification pass, (3) matching topic produces observably different output from the no-topic baseline (positive control for the boost being active). The byte-equality check is what Codex specifically asked for in Turn 1 — it is the only test shape that proves the fail-open contract survives the full hook pipeline and not just the isolatedapplyTopicBoostunit.
Codex review
GPT 5.4 High, session 019d72d5 (continues the session chain used from v0.7.1 through v0.8.5, cumulative ~7.5M tokens across 16+ turns). Design review cleared after 6 turns across both §11.4 and §11.1 together. Implementation reviews ran sequentially:
§11.4 implementation — cleared in 2 turns. Turn 1 raised 1 High finding: the zero-match fail-open contract was violated because
applyTopicBoostdemoted every non-matching doc uniformly even when ZERO docs matched the topic, which under the adaptive threshold filter downstream would push borderline docs below the threshold and silently shrink the result set. Fixed with pre-computed match array + early-return no-op + hook-level integration test asserting byte-equality between the no-topic baseline and a non-matching-topic run. Turn 2 zero findings.§11.1 implementation — cleared in 2 turns. Turn 1 raised 1 Medium finding:
<vault-facts>could duplicate the same fact when the prompt resolves both endpoints of one triple, becausestore.queryEntityTriples(entityId)defaults todirection='both'and the block had no cross-entity dedupe — the same line would appear twice and spend budget twice. Fixed with cross-entity(subject, predicate, object)dedup Set applied before budgeting + 2 new unit tests (same triple from both endpoints → one line; distinct triples sharing one endpoint → both lines preserved). Turn 2 zero findings, verbatim: "No remaining findings on §11.1. Yes, §11.1 is now cleared to ship."
All nine design approvals from the design-phase review survived implementation unchanged (three-path extraction, validate-then-count ordering, cross-path entity-id dedup, longer-n-gram tie-breaker, stage placement after vaultInner, index migration safety, the four accepted design deviations).
Test coverage
+96 tests across the unit + integration layers on top of the v0.8.5 baseline:
tests/unit/session-focus.test.ts(51 tests) — focus file round-trip,resolveSessionTopicenv-var-precedence,applyTopicBoostboost/demote math, demote floor clamp, zero-match NO-OP, empty-array early return, Unicode topic handling, file-size overflow guard.tests/unit/vault-facts.test.ts(49 tests) —extractCanonicalIdsregex boundary cases (interior hyphens, trailing hyphens, word boundaries),extractProperNounstitle-case sequences,generateNgramCandidateslonger-first ordering + length tagging + dedup,batchLookupNamesvault isolation,extractPromptEntitiesthree-path orchestration + path-(a/b/c) happy paths + validate-first invariant against long capitalized noise + exact-100 boundary + cross-path dedup + ambiguity skip,buildVaultFactsBlockbudget handling + truncation at triple boundary + empty-block drop + per-entity max cap + fail-open on DB throw + cross-entity dedup + distinct-triples-sharing-endpoint preservation + schema migrationPRAGMA index_listassertion.tests/integration/context-surfacing-topic-boost.integration.test.ts(3 tests) — zero-match fail-open byte-equality × 2 variants (single-result + multi-result), matching topic observably changes output vs the no-topic baseline.
Public test suite: 1025 → 1105, zero regressions. Skill-forge clawmem backport test suite: 1022 pass, zero regressions (53 files, smaller baseline because skill-forge has fewer test files overall).
Upgrading from a pre-v0.9.0 vault
v0.9.0 is safe to drop in — the only behavior change on existing code paths is the additive <vault-facts> block and the session-scoped topic boost, both gated on new state (entity seeds from the prompt and a per-session focus file). Everything else is unchanged:
Schema — the only migration is a single
CREATE INDEX IF NOT EXISTS idx_entity_nodes_lower_namestatement that runs idempotently on first open. No column additions, no data rewrites, no downtime.API — zero breaking changes. New
clawmem focussubcommand is additive. Existing CLI commands and MCP tools are signature-compatible.Config / env vars — optional:
CLAWMEM_FOCUS_ROOT(override for the focus file root, primarily for hermetic testing) andCLAWMEM_SESSION_FOCUS(debug-only override, NOT session-scoped). The newfactsTokensprofile field defaults to 0 / 200 / 250 for speed/balanced/deep — existing custom profiles that don't set it will receivefactsTokens: 0from the type default (stage disabled) unless you add it explicitly.Hooks — no
clawmem setup hooksre-run needed. Claude Code invokes${binPath} hook ${name}at runtime, so upgrading the ClawMem binary alone propagates the new context-surfacing behavior.Reindex / embed — not required.
<vault-facts>reads from existingentity_triplesrows populated by the v0.8.5 SPO extraction pipeline, and §11.4 reads from existingdocumentsrows. No re-enrichment needed to see the new blocks.
Confirming the features are live:
clawmem focus set "authentication" --session-id test1 && clawmem focus show --session-id test1should print the topic and the expected file path at~/.cache/clawmem/sessions/test1.focus.After a user prompt that mentions a known entity (e.g., one that appears in
sqlite3 ~/.cache/clawmem/index.sqlite "SELECT name FROM entity_nodes LIMIT 5"), the hook should append a<vault-facts>block inside<vault-context>onbalanced/deepprofiles. Check by runningecho "tell me about <entity>" | clawmem surface --context --stdin.
v0.8.5 — SPO Triple Extraction Fix: KG Actually Populates Now
Fixes a bug cluster (BACKLOG.md §1.6) where entity_triples was stuck at zero rows on production vaults regardless of activity, making kg_query return empty for every entity and silently hollowing out the WHY / ENTITY graph-traversal paths in intent_search. Nine bugs across the decision-extractor → observer → entity-resolution → triple-storage pipeline were traced and fixed across four Codex review turns. The fix is entirely additive and strictly improves pre-v0.8.5 behavior — no schema changes, no API breaks, no required migration steps.
LLM-based SPO extraction replaces the old regex path — the observer LLM now emits structured
<triples>blocks alongside<facts>, parsed and validated inparseObservationXmlagainst a fixed predicate vocabulary. The pre-v0.8.5 regex-basedextractTripleFromFactrequiredsubject verb objectsentence shape, which rejected the majority of real observation facts (descriptive phrases like "ClawMem now deploys via systemd user units on a remote host"). The LLM path extracts relational claims without sentence-shape constraints.VALID_PREDICATESis a tight 13-predicate set —adopted,migrated_to,deployed_to,runs_on,replaced,depends_on,integrates_with,uses,prefers,avoids,caused_by,resolved_by,owned_by— and the parser rejects anything outside it.LITERAL_PREDICATESmarksprefers/avoidsas literal-object predicates (object is stored as a string, not resolved to an entity).Canonical A-MEM entity IDs end-to-end via
ensureEntityCanonical— the old path mintedentity_nodesrows withentity_type='auto'(not a valid compatibility bucket, so those entities never resolved viakg_query— a major root cause of the empty KG). The new helper insrc/entity.tsresolves to a canonicalvault:type:slugentity ID shared with the rest of A-MEM, never writes'auto', and — unlikeupsertEntity— does NOT bumpmention_count, so SPO triple references don't inflate A-MEM's doc-mention counter.INSERT OR IGNORE+ deterministicmakeEntityIdhandles concurrent-insert races correctly.Ambiguity-safe type inheritance via
resolveEntityTypeExact— when the observer emits a bare entity name, the helper inherits itsentity_typefromentity_nodesonly if exactly one entity in the vault matches. Zero matches return null (caller defaults toconcept); multiple matches across buckets (e.g., "Alice" aspersonAND asproject) also return null instead of arbitrarily picking.SELECT DISTINCT entity_type ... WHERE LOWER(name) = LOWER(?)— exact-match-only, no fuzzy fallback, fails closed on ambiguity.Observation type gate widened to
{decision, preference, milestone, problem, discovery, feature}— the pre-v0.8.5 gate rejected roughly 77% of real observations in production vaults because it only accepteddecision/preference/milestone/problemwhile the majority of observer output wasdiscovery. The newSPO_ELIGIBLE_OBSERVATION_TYPESset includesdiscoveryandfeature(the two most common types for product-development work) while still excludingrefactor/bugfix/change(noisy types that would dilute the KG).Observation path collision fix — the persistence path was previously
observations/${date}-${session8}-${type}.md, which collided onUNIQUE(collection, path)whenever two observations of the same type appeared in one session (common fordiscoveryduring deep coding sessions). The secondinsertDocumentthrew, was silently caught, and the observation + its triples were dropped. The new path bakes an 8-char SHA256obsHashslice into the filename — deterministic (identical bodies still collide → idempotent reruns), collision-safe under a birthday bound for realistic session sizes. Persistence logic is now in an exportedpersistObservationDochelper so regressions are unit-testable.Placeholder leak defense at both prompt and parser — the pre-v0.8.5
OBSERVATION_SYSTEM_PROMPTplaced literal example text (<fact>Individual atomic fact</fact>) inside tags the parser later extracted, and a weak 1.7B model occasionally echoed that text verbatim into productionentity_triplesrows. The new prompt uses ellipsis placeholders outside data tags, prose field rules below the XML block, and two parser defenses: an exact-string blocklist (SCHEMA_PLACEHOLDER_STRINGS) + a narrow shape-only regex ({{...}},<!--...-->,${...}). Applied totitle, every<fact>, and every triple<subject>/<object>.kg_querycanonical-ID fallback — previously, a callerside canonical ID likedefault:project:clawmemwas run throughsearchEntities, failed to match as free text, then fed through a slugification fallback (entity.toLowerCase().replace(/[^a-z0-9]+/g, "_")) that fabricated invalid IDs and returned misleading "no facts found for X" responses. The fallback now regex-tests for the canonical shape (/^[a-z][a-z0-9-]*:[a-z_]+:[a-z0-9_]+$/) and round-trips the ID verbatim if it matches, otherwise returns an explicit "no entity found" message with a format hint. The tool description + parameter description updated to advertise canonical-ID acceptance.kg_queryis a pure superset — entity-name callers see no behavior change.source_doc_idprovenance on every triple —addTriplecallsites now passsourceDocId: wit.docIdfrom the persisted observation, enabling downstream provenance queries and joining triples back to their originating observation document. Previously dropped silently.source_factis the reconstructedsubject predicate objectstring (notJSON.stringify) so human inspection is readable.Deleted false-comfort test + new real coverage — the pre-existing
tests/unit/triple-extraction.test.tswas a copy-paste of the (now deleted) regex helper, not testing production code. Replaced withtests/unit/spo-extraction.test.tscovering the full new pipeline: parser triple extraction (canonical predicates, unknown predicate rejection, missing-field rejection, oversize rejection, placeholder rejection, 5-triple cap, case normalization), placeholder filtering (exact strings + template shapes + legitimateExample:false-positive preservation),VALID_PREDICATES/LITERAL_PREDICATESsanity,ensureEntityCanonical(canonical creation, zero mention_count bump, reuse, vault scoping, no'auto'leak),resolveEntityTypeExact(single match, ambiguity rejection, vault isolation), end-to-end triple insertion via canonical IDs with realsource_doc_id, andpersistObservationDoccollision-free + idempotent + fact-less behavior.Codex review — GPT 5.4 High, session
019d72d5(same session used for v0.7.1 through v0.8.4), 4 turns of review-and-fix. Turn 1 raised 3 categories of bugs and a design direction. Turn 2 raised 3 High findings on the Turn 1 fix plan (upsertEntitybumping mention_count,resolveEntityTypeambiguity unsafety, docId-threading path assumption) plus tighter predicate vocabulary recommendation. Turn 3 raised 1 High (observation path collision, a pre-existing blocker that the new pipeline surfaced) + 1 Low (placeholder regex false-positive onExample:prefix) plus a recommended integration test. Turn 4: zero remaining findings, clear to ship.
Adds +34 tests (tests/unit/spo-extraction.test.ts, 31 Turn-3 parser/entity/provenance + 3 Turn-4 collision-free persistence) on top of the v0.8.4 baseline. Public test suite: 1006 → 1025, zero regressions.
Upgrading from a pre-v0.8.5 vault
v0.8.5 is safe to drop in — the fix is additive across the board and nothing existing breaks:
Schema — zero changes. No SQL migration runs on first open.
API — zero breaking changes.
kg_querygained canonical-ID acceptance but continues to accept entity names; every other MCP tool signature is identical.Config / env vars — zero changes.
Hooks — no
clawmem setup hooksre-run needed. Claude Code invokes${binPath} hook ${name}at runtime, so upgrading the ClawMem binary alone propagates the new decision-extractor behavior.Data cleanup — optional. Dead
entity_nodes.entity_type='auto'rows and placeholdersource_factstrings from pre-v0.8.5 runs are harmless (they never resolve viakg_query), but can be deleted if you want a clean slate. See the "kg_query returns empty for every entity" entry indocs/troubleshooting.mdfor the exactsqlite3cleanup commands and diagnostic symptoms.Retroactive re-enrichment — optional. v0.8.5 does not introduce new enrichment stages, so
clawmem reindex --enrichis NOT required. Only run it if you specifically want triple extraction to re-fire across your existing observation history — but note that past observation transcripts are gone, so re-enrichment on already-persisted_clawmem/observations/*.mdfiles will not recover lost same-type-collision observations. New Stop-hook activity from v0.8.5 onward is the cleanest source of triples. (Correction, v0.43.2:reindex --enrichnever runs triple extraction, and it re-extracts entities only for documents that are new or changed since their last extraction; see enrichment lifecycle.)
Confirming the fix is live — after a real Claude Code Stop-hook-firing session, sqlite3 ~/.cache/clawmem/index.sqlite "SELECT source_fact FROM entity_triples ORDER BY created_at DESC LIMIT 5" should show human-readable subject predicate object strings (e.g. "ClawMem depends_on Bun"), not JSON blobs or schema-placeholder echoes.
v0.8.4 — OpenClaw Setup Auto-Install + Active Memory Coexistence Docs
Fixes the OpenClaw plugin setup workflow that caused issue #5 ("plugin not found: clawmem"). Patch release — no schema changes, no migration required.
clawmem setup openclawnow auto-installs — previously only printed manual instructions (symlink + manifest copy + config set), which users frequently skipped or misconfigured. Now auto-creates~/.openclaw/extensions/clawmemas a symlink to the plugin source, verifies the manifest exists, and prints only the remaining steps that require a gateway restart first (slot assignment, GPU endpoints, REST API). Handles stale symlinks (detects viareadlinkcompare, replaces automatically after npm updates), existing directories (removes and re-symlinks), and regular file conflicts (aborts with clear message). Idempotent on re-run.clawmem setup openclaw --removenow auto-uninstalls — previously only printed removal instructions. Now removes the symlink/directory and resets the context engine slot tolegacyviaopenclaw config set(if the OpenClaw CLI is available). Falls back to printing the manual command when the CLI is absent.Manifest renamed to
openclaw.plugin.json— the plugin manifest shipped asplugin.jsonbut OpenClaw expectsopenclaw.plugin.json. The old setup workflow worked around this with a copy step that wrote into the symlinked source tree (bad for protected npm prefixes and source checkouts). Now ships with the correct filename, eliminating the copy step entirely.OpenClaw v2026.4.10+ version warning — setup prints a warning about the config normalization bug (openclaw/openclaw#64192) where
plugins.slots.contextEnginewas silently dropped on earlier versions. The warning is informational, not fatal.Active Memory coexistence documented — new section in the OpenClaw plugin guide explaining that ClawMem and OpenClaw's Active Memory plugin (v2026.4.10+) are fully compatible: different plugin kinds, different injection targets (user prompt vs system prompt), different memory backends. Both can run simultaneously.
Codex review — GPT 5.4 High, session
019d72d5, turns 25-27 (~4.32M cumulative tokens). Turn 25 raised 1 High (manifest copy into source tree) + 2 Medium (auto-install gap, version warning). Turn 26 raised 1 High (config set before gateway restart) + 1 Low (regular file conflict unhandled). Turn 27: zero remaining findings.
No breaking changes. Existing users who previously ran the manual symlink steps are unaffected — setup openclaw detects the existing correct symlink and skips re-creation.
External credit
@saschabuehrle — opened yoloshii/ClawMem#6 with the gateway-restart-before-slot-assignment fix and the correct diagnosis of the
Context engine "clawmem" is not registeredfailure path. The PR was closed as superseded because v0.8.4 ended up shipping a broader auto-install fix, but the restart-ordering insight is preserved verbatim in v0.8.4's printed next-steps and the code comment that documents the constraint.
v0.8.3 — Content-Type-Aware Entity Cap + Self-Loop Guard + Docs Restructure
Two small safety/correctness fixes land alongside a major documentation restructure. Patch release — no schema changes, no migration required, no new env vars.
Content-type-aware entity cap — A-MEM entity extraction used a flat
.slice(0, 10)cap that silently dropped legitimate entities on long-form content (research dumps, conversation synthesis, hub/index documents). The newENTITY_CAP_BY_TYPEmapping insrc/entity.tsscales the cap bycontent_type: research documents keep up to 15, hub and conversation documents keep up to 12, short types (decision, deductive, note, handoff, progress) stay at 8, and anything else — including untyped documents and unknown types — keeps the pre-v0.8.3 default of 10.extractEntitiesgained an optionalcontentTypeparameter andenrichDocumentEntitiesthreads the column through from the document row. The LLM extraction prompt also advertises the dynamic cap directly (0-${cap} entitiesinstead of the old hardcoded0-10) so a compliant model no longer stops early on long-form documents. Input is trimmed and lowercased before lookup, so hand-authored frontmatter values like"Research"or" conversation "resolve cleanly.Self-loop guard in
insertRelation— the primarymemory_relationswrite API (store.ts:1545) now rejects relations wherefromDoc === toDocat the API boundary. A self-loop has no informational value for graph traversal and would polluteintent_search/find_similarneighborhoods. A mirror guard was added to the beads dependency bridge insidesyncBeadsIssues(store.ts:~4228) because that path inserts directly intomemory_relationswithout going through the wrapper.buildTemporalBackboneandbuildSemanticGraphwere left alone — both are structurally safe via their loop shape or SQL filter. An extended audit surfaced six additional INSERT sites (inamem.ts,entity.ts,consolidation.ts,conversation-synthesis.ts) that each already have their own structural or explicit self-loop protection; none required new guards.Documentation restructure — All version history was moved out of
README.md(removed 7 inline subsections, ~75 lines) into a new chronologicalRELEASE_NOTES.mdwith every release from v0.1.1 through v0.8.3.README.mdnow reads as setup + usage + architecture, not changelog, and points to the new file. Upgrade action guidance for existing vaults continues to live indocs/guides/upgrading.md. This is the document you are reading.Tests — added 28 new tests across
tests/unit/entity.test.ts(14 unit tests forentityCapForContentTypecovering all content types, defaults, unknown fallback, case/whitespace normalization; 11 integration tests forextractEntitiescovering per-type caps, untyped backward compat, and prompt-shape verification) andtests/unit/openviking-enhancements.test.ts(3 tests for the self-loop guard: plain rejection, mixed-valid-and-self-loop regression, and upsert weight-accumulation invariance). Public test suite: 978 → 1006, zero regressions.
No breaking changes. Existing callers that don't pass contentType get the default cap of 10, matching pre-v0.8.3 behavior exactly.
v0.8.2 — Dual-Host Worker Architecture
Both maintenance lanes can now be hosted by the long-lived clawmem watch watcher service in addition to the existing per-session clawmem mcp host. This makes the systemd-managed watcher the canonical 24/7 home for the v0.8.0 heavy maintenance lane — its quiet-window logic finally sees a live worker at the configured hours regardless of whether any Claude Code session is open. The light consolidation lane (Phase 1 backfill + Phase 2 merge + Phase 3 deductive synthesis + Phase 4 recall stats) now also acquires its own DB-backed worker_leases row before each tick, symmetric with the heavy lane's existing exclusivity, so multiple host processes against the same vault cannot race on Phase 2 merges or Phase 3 deductive writes.
Light-lane worker lease —
runConsolidationTickwraps every tick (Phase 1 → 4) inwithWorkerLeaseagainst a newlight-consolidationworker name with a 10-minute TTL. Two host processes (e.g. one watcher service + one per-session stdio MCP) cannot both consolidate the same near-duplicate observations or both INSERT a duplicate row intoconsolidated_observations. Phase 1 enrichment is also serialized — overkill for cost but cleaner for symmetry. The in-processisRunningreentrancy guard remains the cheap first defense before the SQLite lease round-trip.cmdWatchhosts both workers —clawmem watchhonors the sameCLAWMEM_ENABLE_CONSOLIDATIONandCLAWMEM_HEAVY_LANEenv-var gates ascmdMcp. Off by default. Mirror the existing systemd unit (or your wrapper.env) to opt in. The recommended deployment for v0.8.2+ is to set both env vars onclawmem-watcher.serviceand leavecmdMcpunset, so the heavy lane has a continuously available host independent of Claude Code session lifecycle.cmdMcpis now a fallback host with a heavy-lane warning —cmdMcpretains the same env-var gates so non-watcher deployments (e.g. macOS users running everything via Claude Code launchd) keep working unchanged. WhenCLAWMEM_HEAVY_LANE=trueis set on a stdio MCP host,cmdMcpemits a one-line warning to stderr advising operators to move heavy-lane hosting toclawmem watchinstead.Async drain on shutdown — both worker stop helpers (
stopConsolidationWorkerand the closure returned bystartHeavyMaintenanceWorker) are nowasync, clearing theirsetIntervalAND polling their in-flight running flag until any mid-tick worker drains. This guarantees the worker'swithWorkerLeasefinally block runs against a still-open store, so the lease is released cleanly instead of leaking until TTL expiry. Bounded waits (15s light, 30s heavy) prevent a stuck tick from wedging shutdown indefinitely; the next process reclaims any stranded lease atomically.Signal handlers registered before worker startup — both
cmdWatchandcmdMcpnow register theirSIGINT/SIGTERMhandlers BEFORE any worker initialization. A signal arriving in the brief window between worker startup and handler registration would otherwise terminate the host via the default signal action (exit 143) and skip the async drain entirely.Subprocess smoke test — new
tests/integration/cmdwatch-workers.integration.test.tsspawnsbun src/clawmem.ts watchagainst a temp vault with short worker intervals, exercises the env-var gates, exercises a real heavy-lane tick (slow path, ~35s), and asserts the lease is released cleanly onSIGTERM.Bug fix: removed dead skill-vault watcher block from
clawmem.ts cmdWatch()— a try/catch wrapped block had been silently destructuringgetSkillContentRootfrom./config.ts, but that helper is forge-internal and was never exported in public ClawMem. The runtime catch swallowed the failure so it had no observable effect, but TypeScript flagged a staticTS2339error on the destructure. v0.8.2 removes the dead code path. No behavior change for public users.
Adds +15 tests (9 light-lane lease unit + 5 cmdWatch fast subprocess + 1 cmdWatch slow subprocess) on top of the v0.8.1 baseline.
For operational guidance — enabling the workers via systemd drop-in, tuning intervals to your usage pattern, monitoring queries, and rollback steps — see docs/guides/systemd-services.md.
v0.8.1 — Multi-Turn Prior-Query Lookback
context-surfacing now builds its retrieval query from the current prompt plus up to two recent same-session prior prompts, so a short follow-up turn ("do the same for X", "explain the rationale") can still inherit the vocabulary of earlier turns. The raw prompt is persisted in a new nullable context_usage.query_text column so future hook ticks can reconstitute the multi-turn query from the DB. See multi-turn lookback for the full walkthrough.
Additive schema migration — new nullable
query_text TEXTcolumn oncontext_usage, guarded byPRAGMA table_info. Pre-v0.8.1 stores get the column added on first open; ad-hoc stores that skip the migration path degrade transparently via a feature-detect WeakMap soinsertUsageFnnever writes a column that doesn't exist.Discovery path only — the multi-turn query feeds vector search, BM25, and query expansion. Cross-encoder reranking continues to use the RAW current prompt so relevance scoring is not diluted by older turns, and composite scoring / snippet extraction / dedupe / routing-hint detection all remain on the raw prompt as well.
Privacy-conscious persistence split — gated skip paths (slash commands,
MIN_PROMPT_LENGTH,shouldSkipRetrieval, heartbeat dedupe) do NOT persist their raw text because those turns are not meaningful user questions and carry a higher sensitivity profile. Post-retrieval empty paths (empty result set, threshold blocked, budget blocked) DO persist so a follow-up turn can still inherit the intent even when the current turn surfaced nothing.Current-first truncation — the combined query is clamped to 2000 chars with the current prompt preserved verbatim at the head. Older priors are dropped first when the budget runs out. If the current prompt alone already exceeds the cap, priors are omitted entirely and the current prompt is truncated.
SQL-level self-match guard — duplicate submits of the same prompt are filtered out of the lookback SELECT via
AND query_text != ?so a retry burst cannot eat into the 2-prior budget and leave the lookback window underfilled.10-minute max age, session-scoped — priors older than 10 minutes or from a different
session_idare invisible to the lookback. All fallback paths (missing column, DB error, no matching rows) return the current prompt unchanged — the hook never throws on lookback failures.
Adds +27 tests (22 unit + 5 integration) on top of the v0.8.0 baseline.
v0.8.0 — Quiet-Window Heavy Maintenance Lane
A second, longer-interval consolidation worker that keeps Phase 2 + Phase 3 running on large vaults without starving interactive sessions. Off by default — set CLAWMEM_HEAVY_LANE=true to enable. The existing 5-minute light-lane worker is unchanged. See heavy maintenance lane for the architectural walkthrough.
Quiet-window gating — the heavy lane only runs inside the hours set by
CLAWMEM_HEAVY_LANE_WINDOW_START/CLAWMEM_HEAVY_LANE_WINDOW_END(0-23). Supports midnight wraparound (e.g., 22→6). Null on either bound means "always in window".Query-rate gating via
context_usage— counts hook injections in the last 10 minutes and skips the tick when the rate exceedsCLAWMEM_HEAVY_LANE_MAX_USAGES(default 30). No newquery_activitytable; reuses v0.7.0 telemetry.DB-backed worker leases — exclusivity enforced via a new
worker_leasestable with atomicINSERT ... ON CONFLICT DO UPDATE ... WHERE expires_at <= ?acquisition, random 16-byte fencing tokens, and TTL reclaim. Safe under multi-process contention; any SQLite error translates to alease_unavailableskip rather than a thrown exception.Stale-first selection — Phase 2 and Phase 3 reorder their candidate sets by
COALESCE(recall_stats.last_recalled_at, documents.last_accessed_at, documents.modified_at) ASCso long-unseen docs bubble up first. Emptyrecall_statsfalls through to access-time without erroring.Optional surprisal selector —
CLAWMEM_HEAVY_LANE_SURPRISAL=trueplumbs k-NN anomaly-ranked doc ids (via the existingcomputeSurprisalScores) into Phase 2 as an explicitcandidateIdsfilter. Degrades to stale-first on vaults without embeddings and logsselector: 'surprisal-fallback-stale'in the journal.maintenance_runsjournal — every scheduled attempt writes a row:status(started/completed/failed/skipped),reasonfor skips, selected/processed/created/null_call counts, and ametrics_jsonpayload with selector type and fullDeductiveSynthesisStatsbreakdown. Operators can reconstruct any lane decision without reading worker logs.Force-enforce merge gate — the heavy lane passes
guarded: truetoconsolidateObservations, which overridesCLAWMEM_MERGE_GUARD_DRY_RUNinsidefindSimilarConsolidationso experimenting operators cannot weaken heavy-lane enforcement via env flag.
Adds +56 tests (13 worker-lease + 35 maintenance unit + 8 maintenance integration) on top of the v0.7.2 baseline.
v0.7.2 — Post-Import Conversation Synthesis
Opt-in LLM pass that runs after clawmem mine finishes indexing an imported collection. Operates on the freshly imported content_type='conversation' documents and extracts structured knowledge facts (decisions / preferences / milestones / problems) plus cross-fact relations, writing each fact as a first-class searchable document alongside the raw conversation exchanges. See post-import synthesis for the architectural walkthrough.
New CLI flag —
clawmem mine <dir> --synthesize [--synthesis-max-docs N]. Off by default. When omitted, existing mine behaviour is byte-identical to v0.7.1.Two-pass pipeline — Pass 1 extracts facts per conversation via the existing LLM, saves each via dedup-aware
saveMemory, and populates a local alias map. Pass 2 resolves cross-fact links against the local map first, falling back to collection-scoped SQL lookup. Forward references (link to a fact extracted later in the same run) are resolved correctly.Idempotent reruns — synthesized fact paths are a pure function of
(sourceDocId, slug(title), short sha256(normalizedTitle)), so reruns over the same conversation batch hit thesaveMemoryupdate branch instead of creating parallel rows. Same-slug collisions are disambiguated by the stable hash suffix, not encounter order.Fail-closed link resolution — when two different facts claim the same normalized title or alias, the resolver treats the link as ambiguous and counts it unresolved. Pre-existing docs with duplicate titles in the collection do not silently bind either.
Weight-monotonic relation upsert —
memory_relationsinsert usesON CONFLICT DO UPDATE SET weight = MAX(weight, excluded.weight), which is idempotent on equal-weight reruns but still accepts stronger later evidence without double-counting.Non-fatal failure model — any LLM failure, JSON parse error, saveMemory collision, or relation insert error is counted and logged, never re-thrown. Synthesis failure after
indexCollectioncommits does not roll back the mine import.Split operator counters —
llmFailurescounts actual LLM path failures (null, thrown, non-array JSON), whiledocsWithNoFactscounts docs where the LLM responded validly but returned zero structured facts. Previously these were conflated asnullCalls.
Adds +63 tests (46 unit + 5 integration + 12 regression) on top of the v0.7.1 baseline.
v0.7.1 — Safety Release
Five independent safety gates around the consolidation pipeline and context surfacing, aimed at preventing contamination, cross-entity merges, and unchecked contradictions from landing in the vault. Every extraction ships with full unit + integration test coverage (+158 tests on top of the v0.7.0 baseline). See consolidation safety for the architectural walkthrough.
Taxonomy cleanup — standardized on the A-MEM
contradicts(plural) convention across the entire codebase, eliminating silent query misses on the legacy singular formName-aware merge safety — the Phase 2 consolidation worker gate extracts entity anchors (via
entity_mentions, with lexical proper-noun fallback) and runs dual-threshold normalized 3-gram cosine similarity before merging similar observations. Cross-entity merges are hard-rejected when anchor sets differ materially, preventing context bleed where "Alice decided X" merges into "Bob decided X". Thresholds are env-overridable (CLAWMEM_MERGE_SCORE_NORMAL=0.93,_STRICT=0.98). Dry-run mode viaCLAWMEM_MERGE_GUARD_DRY_RUNfor calibration.Contradiction-aware merge gate — after the name-aware gate passes, a deterministic heuristic (negation asymmetry, number/date mismatch) plus an LLM check detect contradictory merges. Blocked merges route to
linkpolicy (insert new row +contradictsedge, default) orsupersedepolicy (mark old rowstatus='inactive'). Configurable viaCLAWMEM_CONTRADICTION_POLICYandCLAWMEM_CONTRADICTION_MIN_CONFIDENCE. Phase 3 deductive synthesis applies the same gate to deductive dedupe matches.Anti-contamination deductive synthesis — every Phase 3 draft runs through a three-layer validator: deterministic pre-checks (empty conclusion, invalid source_indices, pool-only entity contamination via
entity_mentions) + LLM validator (fail-open withvalidatorFallbackAcceptscounter) + dedupe. Per-reason rejection stats exposed viaDeductiveSynthesisStatsso Phase 3 yield can be diagnosed without enabling extra logging.Context instruction + relationship snippets —
context-surfacingnow always prepends an<instruction>block framing the surfaced facts as background knowledge the model already holds, and appends an optional<relationships>block listing memory-graph edges where BOTH endpoints are in the surfaced doc set. The relationships block is the first thing dropped when the payload would overflowCLAWMEM_PROFILE's token budget, preserving facts-first behaviour while giving the model graph-level reasoning hooks directly in-prompt.
v0.7.0 — Recall Tracking with Per-Turn Attribution
Extracts recall tracking patterns from OpenClaw's dreaming memory consolidation system. Tracks which documents are surfaced by retrieval, which queries surfaced them, and whether the assistant cited them — per-turn, not per-session. Validated by GPT 5.4 High across 8 review turns (2.6M tokens).
New schema —
recall_events(append-only event log),recall_stats(derived summary with diversity / spacing / negative counts),turn_indexcolumn oncontext_usage+recall_events,contradict_confidencecolumn onmemory_relations.Direct SQLite write in context-surfacing — recall events are written directly from the hook process, not through an in-memory buffer. Claude Code hooks are separate short-lived processes, so in-memory buffering would drop events on every session boundary.
Per-turn attribution —
feedback-loopsegments the transcript into turns and zips withcontext_usagerows byturn_index, checking references per-turn rather than session-globally. Eliminates cross-turn attribution noise where a document surfaced in turn 3 gets credited by a reference in turn 8.Cross-vault support — all 31 MCP tools accept an optional
vaultparameter.context_usagewrites are mirrored into the named vault without cross-DB foreign keys. Newlist_vaults()andvault_sync()tools for vault management. Configured inconfig.yamlundervaults:or viaCLAWMEM_VAULTSenv var.Budget-only recording — only docs that actually made it into the injected context are tracked. Budget-clipped docs are excluded, preventing negative signal inflation from entries the model never saw.
Lifecycle integration —
lifecycle_statusandlifecycle_sweepnow surface pin candidates (high diversity + spacing) and snooze candidates (high noise ratio), scoped to active docs with collection/path in output.SQLite contention fix —
busy_timeout=15sduring DDL init (was 0ms, causing SQLITE_BUSY on concurrent Stop hooks), reset to 5s for normal operations.
New files: src/recall-buffer.ts, src/recall-attribution.ts. Adds +36 recall tracking tests; 659 total passing.
v0.6.0 — Deductive Observations, Surprisal Scoring & LLM Remote Fallback
Consolidation worker gains a Phase 3 that synthesizes higher-order deductive observations from recent related facts. Introduces k-NN surprisal scoring for curator triage, embed-state tracking with retries, and a cooldown-based LLM remote fallback. Honcho deep analysis informed the deductive synthesis and surprisal patterns. GPT 5.4 Codex reviewed across 4 turns, 5.2M tokens. 623 tests passing.
Deductive observation synthesis — the consolidation worker Phase 3 combines related recent observations (
decision/preference/milestone/problem, last 7 days) into first-classcontent_type='deductive'documents withsource_doc_idsprovenance and supporting edges inmemory_relations. Infinite half-life, 0.85 baseline, decay-exempt — deductions compound over time rather than fading.Retrieval separation —
session-bootstrapgetCurrentFocus()surfaces deductive insights in a dedicated "Derived Insights" section.context-surfacingtags them as(deductive)so the agent knows they are synthesized rather than directly observed.Surprisal scoring —
computeSurprisalScores()uses k-NN average-neighbor-distance oversqlite-vecembeddings to identify anomalous observations for curator triage. High surprisal = outlier relative to its semantic neighborhood.Embed-state tracking — documents track
embed_state(pending/synced/failed),embed_error, andembed_attempts. Failed docs retried up to 3 attempts.clearAllEmbeddings()resets state.getHashesNeedingFragments()catches missingseq=0primary embeddings on resume.LLM remote fallback —
generate()andexpandQuery()fall back to localnode-llama-cppon transport failures. Structured failure classification: transport errors trigger a 60s cooldown; HTTP errors andAbortErrordo not. Concurrent race guard via pre-fetch cooldown re-check.
v0.5.1 — Documentation Update
Clarified the LLM fallback description in README — transport vs HTTP error distinction, cooldown semantics, "silently falls back" language replaced with explicit cooldown mechanism description. No behavior changes.
v0.5.0 — Conversation Import & Broadened Observation Taxonomy
New clawmem mine CLI imports conversation exports from six different chat formats. Observation taxonomy expanded from a single "observation" type into four first-class subtypes. GPT 5.4 reviewed across 3 turns, 6 issues found and fixed (consecutive-assistant loss, unawaited writes, permissive plain-text detection, Slack multi-party handling, preference decay exemption, YAML escaping).
clawmem mine <dir>— imports conversation exports from Claude Code, ChatGPT, Claude.ai, Slack, and plain text into the indexing pipeline. Newsrc/normalize.tsformat normalizer supports all six formats with per-format robustness fixes.New
conversationcontent type — 45-day half-life, 0.55 baseline, optimized for chat-log characteristics (shorter-lived relevance than decisions or docs, but longer than handoffs).Three new first-class observation types —
preference(decay-exempt,Infinityhalf-life: user preferences persist indefinitely),milestone(60-day half-life),problem(60-day half-life).Observer prompt updated — the local GGUF observer model now extracts preferences, milestones, and problems explicitly, rather than flattening everything into generic observations.
Decision-extractor dedicated routing — each subtype gets dedicated
content_typetreatment instead of the pre-v0.5.0 flattening into a single "observation" bucket.
v0.4.2 — Gray-Matter Frontmatter Sanitization
Bug fix release. Resolves clawmem update crashes on Obsidian vaults with bare YAML dates or booleans in frontmatter.
gray-matter auto-coerces YAML values — title: 2023-09-27 becomes a Date object, title: true becomes a boolean. Bun's SQLite driver rejects these as bind parameters with "Binding expected string, TypedArray, boolean, number, bigint or null", crashing the indexer mid-run.
Runtime
str()helper inparseDocument()checks all frontmatter fields and coerces to string.Defense-in-depth
safeTitleguards ininsertDocument/updateDocument/reactivateDocumentcatch any Date/boolean leakage past the parse layer.Closes #3.
Affected frontmatter fields: title, domain, workstream, content_type, review_by — any field gray-matter can coerce.
v0.4.0 / v0.4.1 — Version Alignment
Version-number bumps only. No user-visible changes. Kept for npm release continuity between v0.3.4 and v0.4.2.
v0.3.1 — Native Hook Timeout
Bug fix release. Shell timeout wrappers killed hook processes with exit 124 (no stderr), producing Failed with non-blocking status code errors in Claude Code on every hook event.
Native
timeoutproperty —clawmem setup hooksnow generates the native Claude Code hooktimeoutfield instead of wrapping commands in shelltimeout.Stop hooks raised from 10s to 30s — LLM inference in
decision-extractor/handoff-generator/feedback-loopregularly exceeds 10s on CPU or under load.All hooks:
timeoutremoved from command strings — only the new native field carries timeout semantics.Setup documentation updated with new examples. Troubleshooting entry added.
v0.3.2 / v0.3.3 / v0.3.4 are version-number bumps only with no user-visible changes.
v0.3.0 — OpenClaw Compaction Delegation
OpenClaw compatibility release. Reviewed by GPT 5.4 High across 3 turns, 797K tokens.
OpenClaw v2026.3.28+ removed the legacy compaction fallback that compact() implementations with ownsCompaction=false were relying on. ClawMem's OpenClaw plugin needed to delegate compaction to the runtime directly instead of returning compacted: false and expecting OpenClaw to fall back.
compact()delegates to runtime — usesdelegateCompactionToRuntime()fromopenclaw/plugin-sdk/core.precompact-extractstill runs first as a side-effect so pre-compaction state is captured regardless of who performs the compaction.Bootstrap duplication fix —
bootstrap()caches context,before_prompt_buildconsumes it once. Previous behavior invoked bootstrap twice per session.Removed duplicate
before_compactionhook —precompact-extractnow runs once per compaction, not twice.Bootstrap parsing fix — uses
extractContext()instead of the legacysystemMessagefield.New
clearSession()for per-session cleanup.Removed unused
bootstrappedSessions/isBootstrapped()helpers.
Without this fix, OpenClaw sessions never compact on v2026.3.28+.
v0.2.9 — Stop Hook JSON Output Requirement
Documentation-only release. Custom Stop hooks that exit 0 with no stdout cause Claude Code to report Failed with non-blocking status code: No stderr output. Documented the root cause and fix pattern in troubleshooting.md and setup-hooks.md.
v0.2.8 — Stop Hooks on Large Transcripts
Bug fix release. Stop hooks were hanging or OOM-ing on transcripts larger than ~10MB because readTranscript() loaded the entire file.
Backward chunked reader — up to 5× 2MB chunks read backwards with early exit when the target line count is reached. Raw
Bufferaccumulation with a single UTF-8 decode at the end prevents multi-byte character corruption at chunk boundaries.Single
fdwithtry/finally— no descriptor leak.validateTranscriptPath()limit raised from 50MB to 1GB — Claude Code sessions can genuinely produce very large transcripts.
v0.2.7 — Patch Release
Minor fixes. No substantive feature changes beyond v0.2.6.
v0.2.6 — Resilient Lifecycle Tool Search
memory_pin, memory_snooze, and memory_forget now use a 4-stage search cascade instead of BM25-only, preventing No matching memory found failures when the document exists but BM25 fails on multi-term queries.
findMemoryCandidates()cascade — exact path match → BM25 → title-token overlap → vector similarity. Async pipeline with path detection, stopword filtering, minimum match rule (max(2, ceil(n/2))terms), vector fallback on cascade exhaustion.selectLifecycleTarget()confidence gate — ambiguous matches return a candidate list formemory_forget(destructive, requires confirmation), top hit forpin/snooze(non-destructive, single choice).docs/reference/mcp-tools.mdupdated with the new search behavior.
v0.2.5 — Hook SQLITE_BUSY Fix
Bug fix release. Hook SQLite busy_timeout was 500ms while watcher/MCP used 5000ms. During A-MEM enrichment or heavy indexing, watcher write locks exceeded 500ms, causing the hook's DB open to fail with SQLITE_BUSY — surfaced as UserPromptSubmit hook error in Claude Code.
Hook
busy_timeoutraised from 500ms to 5000ms — matches MCP server.Hook still completes within its 8s outer timeout — the raise does not extend user-visible latency, only the patience window.
Troubleshooting docs appended with new entry (existing context preserved).
v0.2.4 — Watcher Inotify FD Exhaustion Fix
Bug fix release. The watcher used fs.watch(recursive: true) which registered inotify watches on every subdirectory, including excluded dirs (gits/, node_modules/, .git/). Broad collection paths like ~/Projects caused 200K+ file descriptors, hanging WSL and triggering inotify limit errors on Linux.
Walk directory trees at startup — skip excluded subtrees using the shared
EXCLUDED_DIRSfromindexer.ts.Watch each non-excluded dir individually (non-recursive) — exact scope instead of blanket recursion.
Safety cap: 500 dirs per collection path — logs a warning if exceeded.
Exported
EXCLUDED_DIRSfromindexer.tsfor watcher to share.Documented in
troubleshooting.md,CLAUDE.md,AGENTS.md,SKILL.md.
Diagnosis command added: ls /proc/$(pgrep -f "clawmem.*watch")/fd | wc -l — healthy watchers stay under 15K FDs.
v0.2.3 — Entity Extraction Quality Overhaul
Entity resolution pipeline rewritten with quality filters, type-agnostic canonical resolution, and IDF-based entity edge scoring. Addresses the v0.2.0 entity extraction quality issues flagged during a spot audit — prompts were producing title-as-entity extractions at 67% rate because the LLM echoed named examples from the prompt.
isLowQualityEntity()filter — rejects title-as-entity (Levenshtein > 0.85), names longer than 60 chars, template placeholders, trailing colons, and invalid locations.Type-agnostic canonical resolution within compatibility buckets —
person/org/locationare isolated;project/service/tool/conceptmerge freely within a sharedtechbucket, capturing the common LLM confusion between them.IDF-based entity edge scoring — rare entities create edges, ubiquitous entities alone cannot. Shared-count bonus for multi-entity overlap across documents.
Prompt rewrite —
0-10 entities(was3-15— upper bound too high invited hallucination), generic placeholder (was named examples the LLM echoed at 67% rate), negative instructions for titles and headings.isValidLocation()— positive-signal only (IP addresses,VM \d+pattern). No length fallback; the old fallback was accepting FQDN-looking heading fragments.clearDocEntityStateguard — handles externally-wiped enrichment state without crashing.LlamaCpp→LLMinterface refactor forentity.tsandintent.tsfunction signatures.New
docs/internals/entity-resolution.mdwith the bucket system and model quality guidance.
v0.2.2 — Entity Enrichment Idempotency
Bug fix release. Entity enrichment could double-count mentions or leave partial state on mid-run failures.
entity_enrichment_statetable with SHA256(title+body) input hash — tracks which documents have been enriched and against what content.Transactional writes — partial failure rolls back; state is only persisted on full success.
Full derived state cleanup on content change — mentions, counts, edges, co-occurrences all cleared when a document's content hash changes.
Concurrent enrichment race protection — re-reads state inside the transaction to catch a racing second enrichment attempt.
Canonical alias dedup before counter mutation — prevents
mention_countinflation when the same entity appears under multiple aliases in one document.Watcher race protection — rechecks input hash after LLM call in case the file changed mid-enrichment.
Zero-entity docs marked enriched — prevents infinite retry on documents the LLM finds no entities in.
v0.2.1 — v0.2.0 Follow-up Fixes
--enrichflag fix —clawmem reindex --enrichnow queues unchanged documents for entity backfill. Previously it only queued changed documents, which meant existing vaults couldn't backfill entities after upgrading.47 new tests for entity resolution, MPFP graph retrieval, temporal UTC boundary handling, observation invalidation, and memory nudge.
Troubleshooting entries for
--enrichvs--forcedistinction and the wrapper bypass trap (scripts runningbun run src/clawmem.tsdirectly miss GPU env var defaults from thebin/clawmemwrapper).
v0.2.0 — Hindsight Pattern Integration
Seven patterns extracted from the Hindsight memory engine plus a memory nudge pattern from Hermes Agent, reviewed by GPT 5.4 High across three rounds. Introduces entity resolution, multi-path graph retrieval, temporal query extraction, 3-tier consolidation, observation invalidation, and memory nudges.
⚠ Migration required: Existing vaults upgrading from v0.1.x must run
clawmem reindex --enrichto populate the new entity tables and trigger A-MEM enrichment on existing documents.reindex --forcealone is NOT sufficient — the A-MEM pipeline skips entity extraction for update-path documents to avoid churn, so the--enrichflag is required to backfill. See docs/guides/upgrading.md and the troubleshooting entry onreindex --force after v0.2.0 upgrade shows no entity extractionfor details.
Entity resolution + co-occurrence graph — LLM entity extraction with quality filters, type-agnostic canonical resolution within compatibility buckets (extensible type vocabulary), IDF-based entity edge scoring, co-occurrence tracking, entity graph traversal for ENTITY intent queries
MPFP graph retrieval — Multi-Path Fact Propagation with meta-path patterns per intent, hop-synchronized edge cache, Forward Push with α=0.15 teleport probability. Replaces single-beam traversal for causal/entity/temporal queries.
Temporal query extraction — regex-based date range extraction from natural language queries ("last week", "March 2026"), wired as WHERE filters into BM25 and vector search
4-way parallel retrieval — temporal proximity and entity graph channels added as parallel RRF legs in
querytool (Tier 3 only), alongside existing BM25 + vector channels3-tier consolidation — facts to observations (auto-generated, with proof_count and trend enum) to mental models. Background worker synthesizes clusters of related observations into consolidated patterns.
Observation invalidation — soft invalidation (invalidated_at/invalidated_by/superseded_by columns). Observations with confidence ≤ 0.2 after contradiction are filtered from search results.
Memory nudge — periodic ephemeral
<vault-nudge>injection prompting lifecycle tool use after N turns of inactivity. Configurable viaCLAWMEM_NUDGE_INTERVAL.
v0.1.8 — Curator-Nudge Backport + Auto-Archive Lifecycle Hook
Curator-nudge event map entry —
HOOK_EVENT_MAPwas missing the curator-nudge hook; it existed only in skill-forge. Backported so public users get the curator-nudge hook wired correctly.Auto-archive lifecycle hook —
staleness-checknow runsgetArchiveCandidates+archiveDocumentson session start when lifecycle policy is configured. Fail-open: any error logs and continues, never blocks session startup.
v0.1.6 — Watcher Session Transcript Exclusion
Bug fix release. The watcher was processing Claude Code session transcript .jsonl files as if they were memory documents, causing SQLite write lock contention that triggered UserPromptSubmit hook error on the context-surfacing hook. Watcher now excludes session transcripts explicitly (only .beads/*.jsonl is still processed).
v0.1.4 / v0.1.5 / v0.1.7 are version-number bumps with minor doc fixes (tool count correction 25 → 28, hook count fixes, manual hook config reference in setup-hooks.md, hook cold start latency notes, stale troubleshooting cleanup).
v0.1.3 — Adaptive Thresholds & Deep Profile Escalation
Context-surfacing moves from absolute score thresholds to ratio-based adaptive thresholds (Tier 1 of the adaptive threshold roadmap). Introduces budget-aware deep-profile escalation that spends remaining hook time budget on query expansion and cross-encoder reranking.
Adaptive ratio-based thresholds — three-layer filtering: activation floor (bail if best result is too weak) + score ratio (keep results within X% of best) + absolute floor (never surface below this). Per profile:
speed0.65/0.24/0.18,balanced0.55/0.20/0.15,deep0.45/0.16/0.12. MCP tools remain on fixed absolute thresholds (agents control their own limits).Deep profile budget-aware escalation — when
CLAWMEM_PROFILE=deepand the fast path (BM25+vector) finishes under 4s, the remaining time budget is spent on (1) query expansion via LLM to discover candidates keyword+vector missed, and (2) cross-encoder reranking of the top 15 candidates for a deeper relevance signal. Hard stop at 6s; fail-open to fast-path results on GPU failure or timeout. Only fires ondeep;speedandbalancedunchanged.deepprofileminScorelowered from 0.35 to 0.25 — composite scoring with recency/confidence decay was filtering out all results at 0.35 for vaults with older documents. Validated end-to-end: deep profile returns results in ~2s, well within the 8s hook timeout.Sort results by score before reranking slice — insertion-order slicing was missing expansion-discovered candidates that ranked highly but arrived later in the pipeline (Codex review finding).
Expanded troubleshooting docs — new entries for snap Bun stdin incompatibility, vector dimension mismatch on fallback model, and balanced vs speed profile retrieval differences.
README + quickstart warnings against snap Bun installation (snap Bun cannot read stdin, breaks hooks).
v0.1.1 / v0.1.2 — Initial Public Releases
First public releases to npm. Baseline feature set:
Hooks + MCP integration for Claude Code —
session-bootstrap,context-surfacing,staleness-check,decision-extractor,handoff-generator,feedback-loop,precompact-extract,postcompact-inject,curator-nudgeA-MEM pipeline — automatic memory note generation, link generation, evolution on document updates
QMD retrieval — BM25 + vector + query expansion + RRF + cross-encoder reranking
MAGMA intent classification + graph traversal — WHY / WHEN / ENTITY / WHAT routing with multi-hop beam search
Composite scoring — half-life decay per content type, attention decay, pin/snooze/forget lifecycle
Watcher + embed timer — systemd user services for continuous vault freshness
Curator agent — 6-phase maintenance workflow for Tier 3 operations agents typically neglect
414 files · 399 not served here
- .env.example (not served here)
- .github/ISSUE_TEMPLATE/bug-report.yml (not served here)
- .github/ISSUE_TEMPLATE/config.yml (not served here)
- .github/ISSUE_TEMPLATE/feature-request.yml (not served here)
- .github/PULL_REQUEST_TEMPLATE.md (not served here)
- .github/social-preview.jpg (not served here)
- .gitignore (not served here)
- AGENTS.md
- ATTRIBUTION.md
- AUTOGINEER-RUNS.md
- CLAUDE.md
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- LICENSE
- README.md
- RELEASE_NOTES.md
- SECURITY.md
- SKILL.md
- agents/clawmem-curator.md (not served here)
- bin/clawmem (not served here)
- bun.lock (not served here)
- bunfig.toml (not served here)
- docs/clawmem-architecture.excalidraw (not served here)
- docs/clawmem-architecture.png (not served here)
- docs/clawmem_hero.jpg (not served here)
- docs/concepts/architecture.md (not served here)
- docs/concepts/composite-scoring.md (not served here)
- docs/concepts/hooks-vs-mcp.md (not served here)
- docs/concepts/multi-vault.md (not served here)
- docs/contributing.md
- docs/guides/cloud-embedding.md (not served here)
- docs/guides/contradiction-invalidation.md (not served here)
- docs/guides/eval-harness.md (not served here)
- docs/guides/hermes-plugin.md (not served here)
- docs/guides/inference-services.md (not served here)
- docs/guides/multi-vault-config.md (not served here)
- docs/guides/openclaw-plugin.md (not served here)
- docs/guides/setup-hooks.md (not served here)
- docs/guides/setup-mcp.md (not served here)
- docs/guides/systemd-services.md (not served here)
- docs/guides/upgrading.md (not served here)
- docs/internals/entity-resolution.md (not served here)
- docs/internals/graph-traversal.md (not served here)
- docs/internals/intent-search-pipeline.md (not served here)
- docs/internals/query-pipeline.md (not served here)
- docs/introduction.md
- docs/quickstart.md
- docs/reference/cli.md (not served here)
- docs/reference/configuration.md (not served here)
- docs/reference/mcp-tools.md (not served here)
- docs/reference/rest-api.md (not served here)
- docs/troubleshooting.md
- eval-bundles/judge-override-2026-08-01/artifacts/haiku-claude-cli-run1.json (not served here)
- eval-bundles/judge-override-2026-08-01/artifacts/haiku-claude-cli-run2.json (not served here)
- eval-bundles/judge-override-2026-08-01/artifacts/sonnet5-claude-cli-run1.json (not served here)
- eval-bundles/judge-override-2026-08-01/tooling/capability-eval.ts (not served here)
- extras/rerankers/zerank-2-seq/.gitignore (not served here)
- extras/rerankers/zerank-2-seq/Dockerfile (not served here)
- extras/rerankers/zerank-2-seq/README.md (not served here)
- extras/rerankers/zerank-2-seq/build_and_verify.py (not served here)
- extras/rerankers/zerank-2-seq/docker-compose.yml (not served here)
- extras/rerankers/zerank-2-seq/server.py (not served here)
- extras/rerankers/zerank-2-seq/zr_common.py (not served here)
- googledd6f95190aef61f7.html (not served here)
- o1-clock-debt.json (not served here)
- o1-seam-debt.json (not served here)
- package.json (not served here)
- scripts/eval-acceptance.ts (not served here)
- scripts/eval-bundle-migrate-copy.ts (not served here)
- scripts/eval-bypass-ab.ts (not served here)
- scripts/eval-composite-ablation.ts (not served here)
- scripts/eval-hook-capture.ts (not served here)
- scripts/eval-hook-stamp-baseline.ts (not served here)
- scripts/eval-keyword-acceptance.ts (not served here)
- scripts/eval-query-template.ts (not served here)
- scripts/experiment-clock.sh (not served here)
- scripts/o1-clock-audit.ts (not served here)
- scripts/o1-seam-audit.ts (not served here)
- scripts/o1-tsconfig.ts (not served here)
- scripts/proximity.ts (not served here)
- scripts/test-enhanced.ts (not served here)
- src/amem.ts (not served here)
- src/beads.ts (not served here)
- src/busy-retry.ts (not served here)
- src/canary.ts (not served here)
- src/causal-reader.ts (not served here)
- src/causal-retrieval.ts (not served here)
- src/causal-writer.ts (not served here)
- src/clawmem.ts (not served here)
- src/clock.ts (not served here)
- src/collections.ts (not served here)
- src/compaction-state.ts (not served here)
- src/config.ts (not served here)
- src/consolidation.ts (not served here)
- src/conversation-synthesis.ts (not served here)
- src/deductive-guardrails.ts (not served here)
- src/directory-context.ts (not served here)
- src/embed-readiness.ts (not served here)
- src/entity-repair.ts (not served here)
- src/entity.ts (not served here)
- src/errors.ts (not served here)
- src/eval/gold.ts (not served here)
- src/eval/hook-gold.ts (not served here)
- src/eval/hook-invariants.ts (not served here)
- src/eval/hook-metrics.ts (not served here)
- src/eval/hook-run.ts (not served here)
- src/eval/hook-trace.ts (not served here)
- src/eval/metrics.ts (not served here)
- src/eval/pair-audit.ts (not served here)
- src/eval/replay.ts (not served here)
- src/eval/replicated.ts (not served here)
- src/eval/report.ts (not served here)
- src/eval/run-identity.ts (not served here)
- src/eval/run.ts (not served here)
- src/eval/types.ts (not served here)
- src/eval/vec-daemon-child.ts (not served here)
- src/formatter.ts (not served here)
- src/graph-traversal.ts (not served here)
- src/health/rerank-golden.json (not served here)
- src/health/rerank-health.ts (not served here)
- src/hermes/__init__.py (not served here)
- src/hermes/plugin.yaml (not served here)
- src/hook-settings.ts (not served here)
- src/hooks.ts (not served here)
- src/hooks/context-surfacing.ts (not served here)
- src/hooks/curator-nudge.ts (not served here)
- src/hooks/decision-extractor.ts (not served here)
- src/hooks/feedback-loop.ts (not served here)
- src/hooks/gate-tokens.ts (not served here)
- src/hooks/handoff-generator.ts (not served here)
- src/hooks/postcompact-inject.ts (not served here)
- src/hooks/precompact-extract.ts (not served here)
- src/hooks/pretool-inject.ts (not served here)
- src/hooks/session-bootstrap.ts (not served here)
- src/hooks/staleness-check.ts (not served here)
- src/hooks/surfacing-bookkeeping.ts (not served here)
- src/hooks/surfacing-fusion.ts (not served here)
- src/indexer.ts (not served here)
- src/intent.ts (not served here)
- src/js-yaml.d.ts (not served here)
- src/judge-audit.ts (not served here)
- src/judge.ts (not served here)
- src/limits.ts (not served here)
- src/llm-retry.ts (not served here)
- src/llm.ts (not served here)
- src/maintenance.ts (not served here)
- src/mcp.ts (not served here)
- src/memory.ts (not served here)
- src/merge-guards.ts (not served here)
- src/mmr.ts (not served here)
- src/normalize.ts (not served here)
- src/observer-reply.ts (not served here)
- src/observer.ts (not served here)
- src/openclaw-paths.ts (not served here)
- src/openclaw/compaction-threshold.ts (not served here)
- src/openclaw/engine.ts (not served here)
- src/openclaw/index.ts (not served here)
- src/openclaw/openclaw.plugin.json (not served here)
- src/openclaw/package.json (not served here)
- src/openclaw/prompt-clean.ts (not served here)
- src/openclaw/session-state.ts (not served here)
- src/openclaw/shell.ts (not served here)
- src/openclaw/tools.ts (not served here)
- src/openclaw/transcript-resolver.ts (not served here)
- src/profile.ts (not served here)
- src/promptguard.ts (not served here)
- src/recall-attribution.ts (not served here)
- src/recall-buffer.ts (not served here)
- src/relation-weight.ts (not served here)
- src/retrieval-gate.ts (not served here)
- src/schema-placeholder.ts (not served here)
- src/scoring-regime.ts (not served here)
- src/search-utils.ts (not served here)
- src/server-guard.ts (not served here)
- src/server.ts (not served here)
- src/session-focus.ts (not served here)
- src/splitter.ts (not served here)
- src/stop-causal.ts (not served here)
- src/stop-checkpoint.ts (not served here)
- src/stop-cursor.ts (not served here)
- src/stop-due.ts (not served here)
- src/stop-extract.ts (not served here)
- src/stop-feedback.ts (not served here)
- src/stop-handoff.ts (not served here)
- src/stop-health.ts (not served here)
- src/stop-hermes-scan.ts (not served here)
- src/stop-identity.ts (not served here)
- src/stop-judge.ts (not served here)
- src/stop-pairing.ts (not served here)
- src/stop-recover.ts (not served here)
- src/stop-repair.ts (not served here)
- src/stop-schema.ts (not served here)
- src/stop-session-docs.ts (not served here)
- src/stop-worker.ts (not served here)
- src/store.ts (not served here)
- src/text-similarity.ts (not served here)
- src/validation.ts (not served here)
- src/vault-facts.ts (not served here)
- src/vector-daemon.ts (not served here)
- src/vector-protocol.ts (not served here)
- src/watcher.ts (not served here)
- src/worker-lease.ts (not served here)
- tests/e2e/consolidate.test.ts (not served here)
- tests/e2e/index-search-score.test.ts (not served here)
- tests/e2e/mine-and-diary.test.ts (not served here)
- tests/e2e/reflect.test.ts (not served here)
- tests/helpers/alignment-driver.ts (not served here)
- tests/helpers/budget-guard-driver.ts (not served here)
- tests/helpers/concurrent-init-worker.ts (not served here)
- tests/helpers/degeneracy-gate-driver.ts (not served here)
- tests/helpers/embed-readiness-fixture.ts (not served here)
- tests/helpers/fake-budget-llm.ts (not served here)
- tests/helpers/fixtures.ts (not served here)
- tests/helpers/mock-llm.ts (not served here)
- tests/helpers/test-store.ts (not served here)
- tests/helpers/w0-guard-driver.ts (not served here)
- tests/hooks/context-surfacing.test.ts (not served here)
- tests/hooks/decision-extractor.test.ts (not served here)
- tests/hooks/doctor-llm-probe.integration.test.ts (not served here)
- tests/hooks/eval-vector-daemon.integration.test.ts (not served here)
- tests/hooks/hook-alignment.integration.test.ts (not served here)
- tests/hooks/hook-lanes.integration.test.ts (not served here)
- tests/hooks/hook-replay.integration.test.ts (not served here)
- tests/hooks/hydrated-projection.integration.test.ts (not served here)
- tests/hooks/o1-vector-leg-terminal.integration.test.ts (not served here)
- tests/hooks/o1-wall-jump.helpers.ts (not served here)
- tests/hooks/o1-wall-jump.integration.test.ts (not served here)
- tests/hooks/session-bootstrap.test.ts (not served here)
- tests/integration/cmdwatch-atomic-save.integration.test.ts (not served here)
- tests/integration/cmdwatch-new-dirs.integration.test.ts (not served here)
- tests/integration/cmdwatch-precheck.integration.test.ts (not served here)
- tests/integration/cmdwatch-workers.integration.test.ts (not served here)
- tests/integration/consolidation-contradictions.integration.test.ts (not served here)
- tests/integration/consolidation-name-guard.integration.test.ts (not served here)
- tests/integration/context-surfacing-multiturn.integration.test.ts (not served here)
- tests/integration/context-surfacing-topic-boost.integration.test.ts (not served here)
- tests/integration/conversation-synthesis-two-pass.integration.test.ts (not served here)
- tests/integration/deductive-guardrails.integration.test.ts (not served here)
- tests/integration/hook-blocking-write.integration.test.ts (not served here)
- tests/integration/intent-db.test.ts (not served here)
- tests/integration/judge-consolidation.integration.test.ts (not served here)
- tests/integration/maintenance-lane.integration.test.ts (not served here)
- tests/integration/rerank-live-model.integration.test.ts (not served here)
- tests/integration/secondary-vault-gate.integration.test.ts (not served here)
- tests/integration/setup-openclaw.integration.test.ts (not served here)
- tests/integration/store-concurrent-init.test.ts (not served here)
- tests/integration/store-search.test.ts (not served here)
- tests/integration/store-session.test.ts (not served here)
- tests/integration/store.test.ts (not served here)
- tests/preload.ts (not served here)
- tests/security/rest-transport.test.ts (not served here)
- tests/security/server-guard.test.ts (not served here)
- tests/smoke.test.ts (not served here)
- tests/unit/amem.test.ts (not served here)
- tests/unit/authored-at-cli.test.ts (not served here)
- tests/unit/authored-at.test.ts (not served here)
- tests/unit/basis-stratum.test.ts (not served here)
- tests/unit/canary-validation.test.ts (not served here)
- tests/unit/causal-boundary.test.ts (not served here)
- tests/unit/causal-migrate.test.ts (not served here)
- tests/unit/causal-reader.test.ts (not served here)
- tests/unit/causal-writer.test.ts (not served here)
- tests/unit/clock-type-negatives.ts (not served here)
- tests/unit/clock-types.test.ts (not served here)
- tests/unit/clock.test.ts (not served here)
- tests/unit/collections-config-comments.test.ts (not served here)
- tests/unit/compaction-cli.test.ts (not served here)
- tests/unit/compaction-doctor.test.ts (not served here)
- tests/unit/compaction-embed.test.ts (not served here)
- tests/unit/compaction-enrichment.test.ts (not served here)
- tests/unit/compaction-fixtures.ts (not served here)
- tests/unit/compaction-leak.test.ts (not served here)
- tests/unit/compaction-mcp.test.ts (not served here)
- tests/unit/compaction-openclaw.test.ts (not served here)
- tests/unit/compaction-setup-hooks.test.ts (not served here)
- tests/unit/compaction-transcript.test.ts (not served here)
- tests/unit/composite-scoring-seam.test.ts (not served here)
- tests/unit/config.test.ts (not served here)
- tests/unit/consolidation-fit-v0414.test.ts (not served here)
- tests/unit/consolidation-lease.test.ts (not served here)
- tests/unit/context-surfacing-6a.test.ts (not served here)
- tests/unit/context-surfacing-6b.test.ts (not served here)
- tests/unit/contradict-taxonomy.test.ts (not served here)
- tests/unit/conversation-synthesis.test.ts (not served here)
- tests/unit/decompose-query.test.ts (not served here)
- tests/unit/deductive-and-embed-state.test.ts (not served here)
- tests/unit/deductive-guardrails.test.ts (not served here)
- tests/unit/doctor-hook-budget.test.ts (not served here)
- tests/unit/doctor-observer-v0412.test.ts (not served here)
- tests/unit/doctor-observer-v0414.test.ts (not served here)
- tests/unit/embed-cooldown-cli.test.ts (not served here)
- tests/unit/embed-cooldown-probe.test.ts (not served here)
- tests/unit/embed-dimension-safety.test.ts (not served here)
- tests/unit/embed-taint-v0412.test.ts (not served here)
- tests/unit/entity-repair-cli.test.ts (not served here)
- tests/unit/entity-repair.test.ts (not served here)
- tests/unit/entity.test.ts (not served here)
- tests/unit/errors.contract.test.ts (not served here)
- tests/unit/errors.test.ts (not served here)
- tests/unit/eval-cli-pair-flags.test.ts (not served here)
- tests/unit/eval-harness.test.ts (not served here)
- tests/unit/experiment-clock.test.ts (not served here)
- tests/unit/frontmatter-engines.test.ts (not served here)
- tests/unit/fts-bypass-knob.test.ts (not served here)
- tests/unit/fts-score.test.ts (not served here)
- tests/unit/hermes-plugin.test.ts (not served here)
- tests/unit/hook-budget-config.test.ts (not served here)
- tests/unit/hook-contract-guards.test.ts (not served here)
- tests/unit/hook-eval-acceptance.test.ts (not served here)
- tests/unit/hook-eval-gold.test.ts (not served here)
- tests/unit/hook-eval-invariants.test.ts (not served here)
- tests/unit/hook-eval-metrics.test.ts (not served here)
- tests/unit/hook-eval-vault-boundary.test.ts (not served here)
- tests/unit/hook-timeout-fix.test.ts (not served here)
- tests/unit/hooks-io.test.ts (not served here)
- tests/unit/indexer-boundary.test.ts (not served here)
- tests/unit/indexer-write-lock.test.ts (not served here)
- tests/unit/indexer.test.ts (not served here)
- tests/unit/intent.test.ts (not served here)
- tests/unit/invalidation.test.ts (not served here)
- tests/unit/judge-audit.test.ts (not served here)
- tests/unit/judge-seam.test.ts (not served here)
- tests/unit/judge.test.ts (not served here)
- tests/unit/kg-query.test.ts (not served here)
- tests/unit/llm-budget-v0412.test.ts (not served here)
- tests/unit/llm-budget-v0414.test.ts (not served here)
- tests/unit/llm-endpoint-probe.test.ts (not served here)
- tests/unit/llm-fallback.test.ts (not served here)
- tests/unit/llm-interface-contract.test.ts (not served here)
- tests/unit/llm-remote-config.test.ts (not served here)
- tests/unit/llm-retry.test.ts (not served here)
- tests/unit/maintenance.test.ts (not served here)
- tests/unit/mcp-routes.test.ts (not served here)
- tests/unit/memory-rank-breakdown.test.ts (not served here)
- tests/unit/memory-stats-rank-tools.test.ts (not served here)
- tests/unit/memory.scoring.test.ts (not served here)
- tests/unit/memory.test.ts (not served here)
- tests/unit/merge-guards.test.ts (not served here)
- tests/unit/mixed-channel.test.ts (not served here)
- tests/unit/mmr.test.ts (not served here)
- tests/unit/mpfp.test.ts (not served here)
- tests/unit/no-hard-delete-tripwire.test.ts (not served here)
- tests/unit/no-hard-delete.test.ts (not served here)
- tests/unit/normalize.test.ts (not served here)
- tests/unit/nudge.test.ts (not served here)
- tests/unit/o1-clock-audit.test.ts (not served here)
- tests/unit/o1-seam-audit.test.ts (not served here)
- tests/unit/observer-budget-v0412.test.ts (not served here)
- tests/unit/observer-fit-69-13.test.ts (not served here)
- tests/unit/observer-format-v0414.test.ts (not served here)
- tests/unit/observer.test.ts (not served here)
- tests/unit/observer.xml-parse.test.ts (not served here)
- tests/unit/openclaw-handlers.test.ts (not served here)
- tests/unit/openclaw-hook-budget.test.ts (not served here)
- tests/unit/openclaw-paths.test.ts (not served here)
- tests/unit/openclaw-plugin.test.ts (not served here)
- tests/unit/openviking-enhancements.test.ts (not served here)
- tests/unit/origin-reconciliation.test.ts (not served here)
- tests/unit/pair-audit.test.ts (not served here)
- tests/unit/promptguard.test.ts (not served here)
- tests/unit/proximity.test.ts (not served here)
- tests/unit/recall-tracking.test.ts (not served here)
- tests/unit/replicated-aggregate.test.ts (not served here)
- tests/unit/rerank-health.test.ts (not served here)
- tests/unit/rerank-local-namespace.test.ts (not served here)
- tests/unit/rest-clients-token.test.ts (not served here)
- tests/unit/retrieval-gate.test.ts (not served here)
- tests/unit/review-t7-v0414.test.ts (not served here)
- tests/unit/schema-placeholder.test.ts (not served here)
- tests/unit/scoring-regime.test.ts (not served here)
- tests/unit/search-utils.blend.test.ts (not served here)
- tests/unit/search-utils.rrf.test.ts (not served here)
- tests/unit/search-utils.test.ts (not served here)
- tests/unit/server.test.ts (not served here)
- tests/unit/session-focus.test.ts (not served here)
- tests/unit/setup-hooks-timeout.test.ts (not served here)
- tests/unit/splitter.fragment-limits.test.ts (not served here)
- tests/unit/splitter.test.ts (not served here)
- tests/unit/spo-extraction.test.ts (not served here)
- tests/unit/spo-triples.test.ts (not served here)
- tests/unit/stop-causal.test.ts (not served here)
- tests/unit/stop-checkpoint-v0412.test.ts (not served here)
- tests/unit/stop-cursor.test.ts (not served here)
- tests/unit/stop-extract.test.ts (not served here)
- tests/unit/stop-feedback.test.ts (not served here)
- tests/unit/stop-fence.test.ts (not served here)
- tests/unit/stop-fixtures.ts (not served here)
- tests/unit/stop-handoff.test.ts (not served here)
- tests/unit/stop-hermes.test.ts (not served here)
- tests/unit/stop-hook-input.test.ts (not served here)
- tests/unit/stop-identity.test.ts (not served here)
- tests/unit/stop-judge.test.ts (not served here)
- tests/unit/stop-observer.test.ts (not served here)
- tests/unit/stop-relations.test.ts (not served here)
- tests/unit/stop-repair.test.ts (not served here)
- tests/unit/stop-replay.test.ts (not served here)
- tests/unit/stop-session-docs.test.ts (not served here)
- tests/unit/stop-t23.test.ts (not served here)
- tests/unit/stop-v0414.test.ts (not served here)
- tests/unit/stop-worker.test.ts (not served here)
- tests/unit/store.macos-sqlite.test.ts (not served here)
- tests/unit/surfacing-bookkeeping.test.ts (not served here)
- tests/unit/surfacing-fusion.test.ts (not served here)
- tests/unit/temporal-utc.test.ts (not served here)
- tests/unit/test-isolation.test.ts (not served here)
- tests/unit/text-similarity.test.ts (not served here)
- tests/unit/turn-starters.test.ts (not served here)
- tests/unit/validation.test.ts (not served here)
- tests/unit/vault-facts.test.ts (not served here)
- tests/unit/vec-exclusion.test.ts (not served here)
- tests/unit/vector-daemon.test.ts (not served here)
- tests/unit/watcher.test.ts (not served here)
- tests/unit/worker-lease.test.ts (not served here)
- tsconfig.json (not served here)
Advantages
- Operates entirely on-device with no API keys or cloud dependencies. evidence: README.md
- Provides seamless cross-runtime memory sharing for different agent types. evidence: README.md
Limitations
- Native Windows support is not recommended; WSL2 is required for full functionality. evidence: README.md
- Requires manual configuration and model management for optimal performance. evidence: README.md
Security
Risk level: Medium risk
This brain runs
Agent instructions that run or fetch something (26)
AGENTS.md:**Always run via `bin/clawmem`** — it sets the endpoints.AGENTS.md:| | `lifecycle_status` / `lifecycle_sweep` / `lifecycle_restore` | Lifecycle stats / archive stale (dry-run default, archives only — ClawMem never deletes rows) / restore.AGENTS.md:**v0.38.0:** the context-surfacing hook is no longer a composite ordering surface — its injected order and admission run on the channel-aware fusion key;AGENTS.md:Through v0.29.0 it permanently deleted archived rows from a non-dry-run sweep and from the SessionStart hook, unreported.AGENTS.md:Re-run `clawmem setup hooks` for the SessionEnd flush;AGENTS.md:Queues older than 24 h → `clawmem repair stop-queue --run`.AGENTS.md:`clawmem repair stop-queue --retry-now held --run` retries held ranges now;AGENTS.md:Run `clawmem embed`.AGENTS.md:Run `build_graphs`.AGENTS.md:**Index-run summary shows a `✎ notes` gap (or `[amem] LLM returned null` per doc)** → the LLM endpoint is dead, squatted by another service, or misconfigured.AGENTS.md:Run `clawmem doctor` — it POSTs a real completion to `CLAWMEM_LLM_URL` and validates the response shape.AGENTS.md:**v0.20.0** closes the residual with an opt-in vector-query daemon (run `clawmem watch`): it runs the blocking sqlite-vec scan off the hook's event loop, so a cold scan times out fast and falls back to FTS instead of blocking the turn — a true hard cap, not just a probability reduction;AGENTS.md:a cold hydrate can no longer run the leg past its deadline after the scan answered.AGENTS.md:❌ Run `status` routinely → ✅ only when retrieval feels broken or after large ingestion.AGENTS.md:Install: `clawmem setup curator`.AGENTS.md:Invoke: **"curate memory" / "run curator" / "memory maintenance"**.SKILL.md:| | `lifecycle_status` / `lifecycle_sweep` / `lifecycle_restore` | Lifecycle stats / archive stale (dry-run default, archives only — ClawMem never deletes rows) / restore auto-archived.SKILL.md:**v0.38.0:** the context-surfacing hook is no longer a composite ordering surface — its injected order and admission run on the channel-aware fusion key;SKILL.md:Through v0.29.0 it permanently deleted archived rows from a non-dry-run sweep and from the SessionStart hook, unreported.SKILL.md:Run `clawmem embed` or wait for the embed timer.SKILL.md:Run `build_graphs` (temporal backbone + semantic edges).SKILL.md:**Index-run summary shows a `✎ notes` gap (or `[amem] LLM returned null` per doc)** → the LLM endpoint is dead, squatted, or misconfigured — `clawmem doctor` shape-probes `CLAWMEM_LLM_URL` with a real completion (v0.37.0).SKILL.md:**v0.20.0** adds the hard cap (run `clawmem watch` — the vector daemon runs the blocking scan off the hook's event loop, so a cold scan falls back to FTS instead of blocking the turn);SKILL.md:Retry held ranges now with `clawmem repair stop-queue --retry-now held --run`;SKILL.md:❌ Run `status` routinely → ✅ only when retrieval feels broken or after large ingestion.SKILL.md:Invoke: **"curate memory" / "run curator" / "memory maintenance"**.
MCP servers (1)
README.md(clawmem):/absolute/path/to/clawmem/bin/clawmem mcp
Install scripts (1)
README.md:- [Bun](https://bun.sh) v1.0+ — runtime for ClawMem. On Linux, install via `curl -fsSL https://bun.sh/install | bash` (not snap — snap Bun cannot read stdin, which breaks hooks).· uses the network
- Medium risk: runs-install-script-remote-exec (README.md:79)
- Low risk: network-instructions (AGENTS.md:33)
- Low risk: scripts-present (src/amem.ts)
- Low risk: scripts-present (src/beads.ts)
- Low risk: scripts-present (src/busy-retry.ts)
- Low risk: scripts-present (src/canary.ts)
- Low risk: scripts-present (src/causal-reader.ts)
- Low risk: scripts-present (src/causal-retrieval.ts)
- Low risk: scripts-present (src/causal-writer.ts)
- Low risk: scripts-present (src/clawmem.ts)
- Low risk: scripts-present (src/clock.ts)
- Low risk: scripts-present (src/collections.ts)
- Low risk: scripts-present (src/compaction-state.ts)
- Low risk: scripts-present (src/config.ts)
- Low risk: scripts-present (src/consolidation.ts)
- Low risk: scripts-present (src/conversation-synthesis.ts)
- Low risk: scripts-present (src/deductive-guardrails.ts)
- Low risk: scripts-present (src/directory-context.ts)
- Low risk: scripts-present (src/embed-readiness.ts)
- Low risk: scripts-present (src/entity-repair.ts)
- Low risk: scripts-present (src/entity.ts)
- Low risk: scripts-present (src/eval/gold.ts)
- Low risk: scripts-present (src/eval/hook-gold.ts)
- Low risk: scripts-present (src/eval/hook-invariants.ts)
- Low risk: scripts-present (src/eval/hook-metrics.ts)
- Low risk: scripts-present (src/eval/hook-run.ts)
- Low risk: scripts-present (src/eval/hook-trace.ts)
- Low risk: scripts-present (src/eval/metrics.ts)
- Low risk: scripts-present (src/eval/pair-audit.ts)
- Low risk: scripts-present (src/eval/replay.ts)
- Low risk: scripts-present (src/eval/replicated.ts)
- Low risk: scripts-present (src/eval/report.ts)
- Low risk: scripts-present (src/eval/run-identity.ts)
- Low risk: scripts-present (src/eval/run.ts)
- Low risk: scripts-present (src/eval/types.ts)
- Low risk: scripts-present (src/eval/vec-daemon-child.ts)
- Low risk: scripts-present (src/formatter.ts)
- Low risk: scripts-present (src/graph-traversal.ts)
- Low risk: scripts-present (src/health/rerank-health.ts)
- Low risk: scripts-present (src/hook-settings.ts)
- Low risk: scripts-present (src/hooks.ts)
- Low risk: scripts-present (src/hooks/context-surfacing.ts)
- Low risk: scripts-present (src/hooks/curator-nudge.ts)
- Low risk: scripts-present (src/hooks/decision-extractor.ts)
- Low risk: scripts-present (src/hooks/feedback-loop.ts)
- Low risk: scripts-present (src/hooks/gate-tokens.ts)
- Low risk: scripts-present (src/hooks/handoff-generator.ts)
- Low risk: scripts-present (src/hooks/postcompact-inject.ts)
- Low risk: scripts-present (src/hooks/precompact-extract.ts)
- Low risk: scripts-present (src/hooks/pretool-inject.ts)
Attribution and licence
- First committed by
yoloshii
- Declared authors
- yoloshii (declared in package.json)
- Publisher
- Yoloshi Nomotomoro? (Community)
- Source
- yoloshii/ClawMem · commit 284b55a
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- yoloshii, cymkd, DrJsPBs, Oliverbot26, inire
AutoGineer is not affiliated with or endorsed by yoloshii.
Install from source
399 of its files are not served here, so there is no ZIP. yoloshii/ClawMem at 284b55a