Skip to main content
Glama
siosig

mcp-chest-memory

by siosig
README.md
# mcp-chest-memory

**English** | [日本語](README.ja.md)

Persistent, cross-session memory for Claude Code — a **lightweight, local-only MCP
server**. Add the MCP and it works anywhere. The MCP process runs **in-process**: it
owns the SQLite store and runs recall directly, and delegates only **embedding** to a
separate, locally-running [`embedding-daemon`](https://github.com/siosig/embedding-daemon)
that is shared across all your sessions and projects. No network backend, no Docker,
no API key for the interactive tools.

> Implemented in Rust. Japanese full-text search uses an embedded dictionary. The
> embedding model (BGE-M3, ~1.7 GB) lives in the external `embedding-daemon`, which
> chest reaches over a local Unix-domain socket (preferred) or loopback TCP — so the
> model is loaded once per host, not once per session. The only network use is the
> optional background Gemini Batch API (classifier + consolidation summarizer);
> interactive memory is fully local and offline.

## Table of contents

- [How it works](#how-it-works)
- [Install](#install)
  - [Background classifier + maintenance (optional)](#background-classifier--maintenance-optional)
- [MCP tools](#mcp-tools)
- [Lifecycle hooks (automatic)](#lifecycle-hooks-automatic)
- [Next-action recommendation](#next-action-recommendation)
- [CLI reference](#cli-reference)
  - [chest-index — maintenance CLI](#chest-index--maintenance-cli)
  - [Hook binaries (internal)](#hook-binaries-internal)
- [/chest-memory skill](#chest-memory-skill)
- [Configuration](#configuration)
- [Single-writer](#single-writer)
- [License](#license)

## How it works

```text
chest_remember ──► staging table (recall can't see it)
                        │  background batch (systemd timer)
                        ▼  classify each item with Gemini → promote / reject
                    memories (searchable)  ◄── chest_recall (hybrid search)
```

- **`chest_remember` never filters or blocks.** It appends your submission to a
  staging area and returns immediately — embedding and classification are
  deferred.
- A **background batch** (run by a systemd timer) classifies staged items with
  the Gemini **Batch API** (batched for low cost) and **promotes** the useful
  ones into searchable memories, **rejecting** noise.
- **`chest_recall`** searches the promoted memories with a hybrid of dense
  (semantic) embeddings and FTS5 full-text search, fused by Reciprocal Rank
  Fusion and weighted by recency and importance.
- **Background maintenance** consolidates cold clusters (summarized by Gemini
  Batch), archives near-duplicates (supersession), and expires TTL memories.
- **In-process, with a shared embedding-daemon.** The MCP server owns the SQLite
  store and runs recall in-process — no chest process loads the embedding model.
  Embedding is delegated to a separate [`embedding-daemon`](https://github.com/siosig/embedding-daemon)
  (a `gemini-embedding-001`-compatible local service) over a Unix-domain socket or
  loopback TCP, so one model instance is shared by every session, the maintenance
  timers, and any other project. Writes stay single-writer: the in-process server
  and the timer-run maintenance CLI serialize via an OS file lock (plus WAL).

## Install

Clone the repo, `cd` into it, and run the installer. It builds the Rust binaries,
puts them on your `PATH`, checks the embedding-daemon is reachable, and installs the
Claude Code plugin.

```bash
git clone https://github.com/siosig/mcp-chest-memory.git
cd mcp-chest-memory
./install_claude_plugin.sh        # build + install
# restart Claude Code
```

Prerequisites: `cargo` (<https://rustup.rs>), `claude` (<https://claude.ai/code>), and
a running [`embedding-daemon`](https://github.com/siosig/embedding-daemon) reachable
over its Unix-domain socket or `127.0.0.1:18181` — install and start it separately
(it is the one process that loads the embedding model). Uninstall with
`./install_claude_plugin.sh -d` (your memories in `~/.chest-memory` are kept).

### Background classifier + maintenance (optional)

To auto-classify and maintain memories on a schedule, deploy the systemd timers
via Ansible (A-phase embed, B-phase classify, C-phase consolidate — each runs
every 5 minutes):

```bash
cd ansible
ansible-playbook site.yml
```

Requires Ansible ≥ 2.15. See [deploy/systemd/README.md](deploy/systemd/README.md)
for details.

Without `GEMINI_API_KEY`, `chest-index up` promotes everything (fail-open) and
falls back to a simple join-based consolidation. With the key, both classification
and consolidation use the Gemini Batch API automatically — no extra flags needed.

## MCP tools

| Tool | Purpose |
| ---- | ------- |
| `chest_remember` | Stage a memory (classified + made searchable by the batch). |
| `chest_recall` | Hybrid dense-semantic + FTS5 search over promoted memories. |
| `chest_update_memory` | Edit a memory's content / importance / layer (re-embeds). |
| `chest_list_entities` | List subjects with their memory counts. |
| `chest_forget` | Archive a memory (protected / high-importance ones are kept). |
| `chest_read_smart` | Read a file, returning "unchanged" when it hasn't changed since last read (saves tokens). |
| `chest_recommend` | Recommend next actions to work on, ranked from accumulated memory (goals, momentum, time-decayed work transitions). |
| `chest_record_recommendation_feedback` | Record accept/reject/ignore feedback so future recommendations improve. |

## Lifecycle hooks (automatic)

The plugin wires Claude Code hooks so memory works without you thinking about it:

- **UserPromptSubmit** → fast FTS-only recall injects relevant memories into the
  prompt (no model load, sub-second).
- **SessionStart** → injects your most active memory topics, and (read-only, no
  model) proactively offers next-action candidates when you start with no
  explicit task — cooldown-gated so it never nags.
- **PreCompact / Stop** → captures your turns into staging before compaction so
  nothing memory-worthy is lost; the batch later triages them.

## Next-action recommendation

Ask the assistant what to do next and it ranks candidates from your accumulated
knowledge — open goals, the topics you're actively working on (momentum), and a
time-decayed model of which topic you tend to move to next — then offers 2–4
concrete next actions as a selection. Picking one starts that task and records
feedback so the ranking improves over time.

- **`/recommend`** — the deterministic path: gathers candidates via
  `chest_recommend` and presents a selection.
- **Proactive** — at session start, the `chest-memory-recommend` hook may offer
  candidates when you have no explicit task (cooldown + per-session cap).

The score layers an additive term on top of the **unchanged** recall composite
(`0.4·composite + 0.3·transition + 0.2·goal + 0.1·momentum`); the transition term
uses a smoothed Markov model that gracefully falls back to associative ranking
until enough history accrues. Algorithm details:
[specs/master/recommend_algorithm.md](specs/master/recommend_algorithm.md).

## CLI reference

### chest-index — maintenance CLI

`chest-index` is the maintenance binary run by the systemd timer (or on demand).

```text
chest-index <subcommand> [flags]
```

| Subcommand | Key flags | What it does |
| ---------- | --------- | ------------ |
| `classify` | `--offline`, `--limit N` | Classify pending staged memories. Without `--offline`, submits a Gemini Batch API job and harvests the previous one; `--offline` promotes everything locally with no network. |
| `up` | `--no-classify`, `--offline`, `--classify-limit N`, `--embed-limit N` | Full maintenance cycle: classify → consolidate → embed backfill → activation decay → supersession → expiry. |
| `consolidate` | `--min-age-days N` | Merge cold, old memory clusters into learning summaries using local join (no network). |
| `import` | `--path PATH` | Import Claude Code conversation history from `~/.claude/projects/` (or a given path) into staging for batch classification. |
| `analyze` | `--days N` | Compute IR quality metrics (precision / recall / F1 / F2 / accuracy) from recall events and print JSON. |
| `usage` | `--days N`, `--all`, `--json` | Show Gemini Batch API token usage with cost. Default: last 24 h hourly table. `--days N` / `--all` switch to daily buckets; `--json` outputs backward-compatible JSON. |
| `fetch-model` | — | Verify the external embedding-daemon is reachable (first-run check). |

**Examples:**

```bash
# classify on demand (Gemini Batch API, async)
chest-index classify

# classify with no network (promote everything)
chest-index classify --offline

# full maintenance cycle
chest-index up

# show token usage for the last 7 days
chest-index usage --days 7

# IR quality report for the last 30 days
chest-index analyze --days 30

# import past Claude Code sessions
chest-index import --path ~/.claude/projects/
```

### Hook binaries (internal)

These binaries are invoked automatically by Claude Code hooks; you rarely need to
call them directly.

| Binary | Trigger | What it does |
| ------ | ------- | ------------ |
| `chest-memory-user-prompt-submit` | `UserPromptSubmit` | Fast FTS-only recall over the prompt; injects matching memories as context (no model load, sub-second). |
| `chest-memory-session-start` | `SessionStart` | Injects the most active memory topics to seed a new session. |
| `chest-memory-recommend` | `SessionStart` | Read-only, no model load: proactively offers next-action candidates when you start with no explicit task (cooldown + per-session cap). |
| `chest-memory-precompact` | `PreCompact` | Captures recent turns into staging before compaction so nothing memory-worthy is lost. |
| `chest-memory-import <file.jsonl>` | Manual / `Stop` hook | Ingests a Claude Code transcript JSONL into staging (file argument). |
| `chest-memory-sync` | `Stop` hook (stdin) | Same as `import` but reads the transcript from stdin (pipe-friendly). |
| `chest-memory-stats` | Manual | Prints a privacy-safe health report: memory counts, staging backlog, recall hit-rate. |

**Check memory health:**

```bash
chest-memory-stats
```

## /chest-memory skill

The plugin ships a `/chest-memory` Claude Code skill that lets you trigger memory
operations interactively from the chat prompt.

| Invocation | What it does |
| ---------- | ------------ |
| `/chest-memory` | Analyze the recent conversation, auto-classify the most valuable fact, save it, and report the rationale. |
| `/chest-memory status` | Show memory store health (total memories, embedding status, whether `chest-index reembed` is needed). |
| `/chest-memory analyze [--days N]` | Show IR quality metrics as a formatted Markdown table (precision / recall / F1 / F2 / accuracy). |
| `/chest-memory <free text>` | Save the given text, still auto-classifying the layer. |

**Memory layers** (auto-selected by the skill):

| Layer | Saved when |
| ----- | ---------- |
| `realize` | An error, trap, or failure that must never recur (protected from auto-forgetting). |
| `learning` | A new insight, decision, or technique that worked. |
| `goal` | A stated objective for the project. |
| `context` | Background / timing facts ("demo on Friday"). |
| `implementation` | Code or config that worked or failed, with specifics. |
| `emotion` | User's emotional state worth calibrating tone to. |

The skill also fires automatically at key moments without being invoked explicitly:
when a new task starts, before editing a file with history, right after resolving a
bug, and whenever you say "remember this" or "did we do this before?".

## Configuration

| Env var | Default | Meaning |
| ------- | ------- | ------- |
| `CHEST_DB` | `~/.chest-memory/chest.db` | SQLite database path. |
| `CHEST_EMBED_SOCK` / `EMBED_SOCK` | `~/.embedding-daemon/embedding.sock`, then `/var/lib/embedding-daemon/embedding.sock` | embedding-daemon Unix-domain socket (preferred when it exists). |
| `CHEST_EMBED_ADDR` / `EMBED_ADDR` | `127.0.0.1:18181` | embedding-daemon loopback TCP address (used when no socket is reachable). |
| `GEMINI_API_KEY` | — | Enables Gemini Batch API for classification and consolidation (else promote-all / join fallback). |
| `CHEST_GEMINI_MODEL` | `gemini-3.1-flash-lite` | Classification model. |
| `CHEST_CONSOLIDATE_MODEL` | `gemini-3.1-flash-lite` | Consolidation summarizer model. |
| `GEMINI_BATCH_SIZE` | `1000` | Items per Gemini batch submit (inline limit is a 20 MB payload, not a count). |
| `EMBED_PARALLELISM` | `4` | Texts per `embed()` call during backfill (batched internally; not OS threads). |
| `EMBED_RETRY_MAX` | `3` | Embed attempts before a memory is marked `failed` (recover with `chest-index reset-failed`). |
| `CHEST_HIT_SCORE_THRESHOLD` | `0.5` | Recall hit threshold for quality stats. |

`chest-memory-stats` prints a privacy-safe report (memory counts, staging
backlog, recall hit-rate). Usage metrics record only lengths, hashes, counts,
and scores — never raw queries or paths.

## Single-writer

Exactly one user is assumed. The in-process MCP server serializes its writes
(remember / update / forget), and the timer-run maintenance CLI serializes against it
with an OS file lock plus WAL + `busy_timeout`; with one user, contention is rare and
the brief WAL wait is acceptable.

## License

[MIT](LICENSE).