flux7-memory
by KTCrisis
README.md
```
______ ____
/ __/ //_ /_____ _ ___ __ _
/ _// /__/ /___/ ' \/ -_) ' \
/_/ /____/_/ /_/_/_/\__/_/_/_/
```
# flux7-memory
[](https://github.com/KTCrisis/flux7-memory/releases)
[](https://github.com/KTCrisis/flux7-memory/actions/workflows/ci.yaml)
[](https://go.dev)
[](LICENSE)
A lightweight MCP server in Go for shared memory across AI agents. Single binary, zero cgo, usable standalone over stdio or as a shared daemon behind [flux7-mesh](https://github.com/KTCrisis/flux7-mesh). Hybrid markdown + SQLite store with full-text search, optional dense-vector hybrid retrieval, LLM reranking, and three transports: MCP stdio, HTTP JSON-RPC, and MCP SSE. Comes with a [Python SDK](#python-sdk) for provider-agnostic integration.
## Features
- **8 MCP tools** — `memory_store`, `memory_recall`, `memory_search`, `memory_context`, `memory_get`, `memory_list`, `memory_forget`, `memory_history`
- **Bi-temporal** — every memory is a series of versions: `valid_from`/`valid_to` on writes, `valid_at` (what held then) and `as_of` (what mem7 believed then) on reads
- **Tamper-evident workspace** — every entry sealed in a hash chain (HMAC with `MEM7_CHAIN_KEY`), checked by `mem7 verify`
- **Hybrid storage** — append-only markdown workspace as source of truth, SQLite (FTS5) as a rebuildable index
- **Field-weighted BM25** — FTS5 ranking with tuned weights: object content (5x), entity key (2x), tags (0.5x)
- **Hybrid search (opt-in)** — BM25 + dense cosine similarity merged via Reciprocal Rank Fusion (RRF). Requires an external embedding provider (Ollama or any OpenAI-compatible API)
- **LLM reranking (opt-in)** — post-RRF listwise reranking via Ollama, with graceful degradation if the reranker is unavailable
- **Natural language mode** — `mode="natural"` strips stop words, applies wildcard stemming, and OR-joins tokens so agents can query in plain language instead of FTS5 syntax
- **Neighbor inclusion** — `include_neighbors=true` automatically fetches sequential neighbors (e.g. `t004`, `t006` around `t005`) to capture context spread across consecutive entries
- **Access tracking** — `access_count` and `last_accessed` are bumped on `memory_recall`, providing usage signals without creating feedback loops
- **Three transports** — MCP stdio (default, for Claude Code / Cursor), HTTP JSON-RPC via `mem7 serve` (for SDKs and direct API calls), and MCP Streamable HTTP on `POST /mcp` (for flux7-mesh daemon mode: one process, shared DB). The older HTTP+SSE pair (`/sse`, `/messages`) remains for existing clients; the MCP spec deprecates it
- **Snapshot reminder** — `POST /memory/snapshot_reminder` (and the matching MCP method) lets an agent runtime inject a pre-compaction instruction into its context
- **Rebuildable index** — `mem7 rescan` drops the SQLite index and replays the markdown workspace to restore consistency
- **Tag filters, agent tracking, TTL**
## Quick start
```bash
go install github.com/KTCrisis/flux7-memory/cmd/mem7@latest
```
Or build from source :
```bash
cd flux7-memory
go build -o ~/go/bin/mem7 ./cmd/mem7
```
Default stdio mode (MCP client spawns the binary) :
```bash
~/go/bin/mem7
```
If a `mem7 serve` daemon is already running, stdio mode auto-detects it and becomes a thin proxy (stdin↔HTTP) instead of opening a second local store. Same command, zero config change.
Daemon mode (shared across multiple clients via HTTP + SSE) :
```bash
MEM7_TOKEN=mem7_secret123 ~/go/bin/mem7 serve --listen :9070
```
Exposes `/rpc` (HTTP JSON-RPC), `/sse` + `/messages` (MCP SSE transport), `/healthz`, and `/memory/snapshot_reminder`. flux7-mesh connects via SSE for MCP tool calls and via `/rpc` for decision writes — one daemon, one database.
Rebuild the SQLite index from the markdown workspace :
```bash
~/go/bin/mem7 rescan
```
Drop TTL-expired entries from the index (the markdown workspace is left untouched ; rescan re-evaluates TTL on replay) :
```bash
~/go/bin/mem7 prune
```
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `MEM7_DIR` | `~/.mem7` | Data directory (hosts `workspace/` and `index.db`) |
| `MEM7_LISTEN` | `:9070` | HTTP bind address when in `serve` mode |
| `MEM7_TOKEN` | *(empty)* | Bearer token required on `/rpc`, `/mcp`, `/sse` and `/memory/*` when set; also what lets a request speak for an agent (see below) |
| `MEM7_SCOPES` | *(empty)* | JSON file of read scopes per agent; empty = reads not scoped |
| `MEM7_CHAIN_KEY` | *(empty)* | Key of the workspace's hash chain (HMAC-SHA256); empty = SHA-256 |
| `MEM7_CHAIN_KEYS_RETIRED` | *(empty)* | Earlier chain keys, comma-separated: they verify the stretches before a `mem7 rekey`, never seal |
| `MEM7_MAX_ENTRIES` | `10000` | Soft ceiling on live entries |
| `MEM7_EMBED_URL` | *(empty)* | Base URL of the embedding provider. Setting this enables hybrid search |
| `MEM7_EMBED_MODEL` | `nomic-embed-text` | Model name passed to the embedding API |
| `MEM7_EMBED_PROVIDER` | `ollama` | Provider format: `ollama` (POST `/api/embed`) or `openai` (POST `/v1/embeddings`) |
| `MEM7_EMBED_KEY` | *(empty)* | Bearer token for the embedding API (required for OpenAI, optional for Ollama) |
| `MEM7_RERANK_URL` | *(empty)* | Base URL of the reranking LLM. Setting this enables LLM reranking after RRF merge |
| `MEM7_RERANK_MODEL` | `gemma4:e4b` | Model name passed to the Ollama generate API for reranking |
Flags on `mem7 serve` mirror `MEM7_LISTEN`, `MEM7_TOKEN` and `MEM7_SCOPES` : `--listen :9070 --token mem7_... --scopes scopes.json`.
To run the daemon as a systemd service, see [`contrib/systemd/mem7.service`](contrib/systemd/mem7.service) (adapt `User=` and paths, then `sudo systemctl enable --now mem7`).
### Hybrid search setup
Hybrid search is entirely opt-in. Without `MEM7_EMBED_URL`, mem7 uses pure BM25.
**With local Ollama :**
```bash
MEM7_EMBED_URL=http://localhost:11434 \
MEM7_EMBED_MODEL=nomic-embed-text \
~/go/bin/mem7
```
**With OpenAI API :**
```bash
MEM7_EMBED_URL=https://api.openai.com \
MEM7_EMBED_MODEL=text-embedding-3-small \
MEM7_EMBED_PROVIDER=openai \
MEM7_EMBED_KEY=sk-... \
~/go/bin/mem7
```
**With any OpenAI-compatible endpoint** (vLLM, LiteLLM, Azure OpenAI, etc.) :
```bash
MEM7_EMBED_URL=http://localhost:8000 \
MEM7_EMBED_MODEL=BAAI/bge-small-en-v1.5 \
MEM7_EMBED_PROVIDER=openai \
~/go/bin/mem7
```
When enabled, `memory_store` computes and persists an embedding alongside each entry. `memory_search` retrieves BM25 top-2N and cosine top-2N candidates, then merges them via Reciprocal Rank Fusion (RRF, k=60) into the final top-N. Embeddings are stored as BLOBs in SQLite and cached in memory for sub-ms cosine search.
### LLM reranking setup
LLM reranking is opt-in on top of hybrid search. It over-fetches 3x candidates, merges via RRF, then uses an LLM to score relevance before returning the final top-N. Falls back to non-reranked results if the LLM is unavailable.
```bash
MEM7_EMBED_URL=http://localhost:11434 \
MEM7_RERANK_URL=http://localhost:11434 \
MEM7_RERANK_MODEL=gemma4:e4b \
~/go/bin/mem7
```
## Python SDK
A provider-agnostic Python client for mem7, wrapping all MCP tools via JSON-RPC over HTTP.
### Install
```bash
pip install flux7-memory
```
Or from source :
```bash
pip install ./sdk/python
```
### Usage
```python
from mem7 import Mem7
m = Mem7("http://localhost:9070", token="my-token")
# Store a memory
m.store("user.prefs", "prefers dark mode", tags=["user"])
# Search (returns formatted text)
print(m.search("dark mode", limit=5))
# Context (returns structured Memory objects)
for mem in m.context("dark mode", limit=5):
print(f"{mem.key}: {mem.value}")
# Formatted block for LLM prompt injection
block = m.context_block("user preferences", limit=10)
# Other tools
m.recall(key="user.prefs")
m.list(tags=["user"])
m.get("memory/2026-05-07.md")
m.forget(key="user.prefs")
```
## Workspace layout
```
~/.mem7/
├── workspace/
│ ├── MEMORY.md # reserved for long-term notes
│ └── memory/
│ ├── 2026-04-11.md # append-only daily logs
│ └── 2026-04-12.md
└── index.db # SQLite (facts + facts_fts + embeddings)
```
The markdown files are the source of truth ; `index.db` is a derived cache that can be dropped and rebuilt from the markdown at any time via `mem7 rescan`.
Each entry is written as a level-2 heading followed by a fenced `mem7` envelope (plain key/value metadata) and a free-form body, terminated by a horizontal rule. A human can edit these files in place — the next `rescan` picks up the changes.
Example :
````markdown
## example_key
```mem7
op: store
agent: claude
tags: demo, example
created: 2026-04-11T20:00:00Z
updated: 2026-04-11T20:00:00Z
```
Free-form markdown content lives here.
---
````
## Usage with flux7-mesh
In your `config.yaml` :
```yaml
mcp_servers:
- name: memory
transport: stdio
command: /home/user/go/bin/mem7
env:
MEM7_DIR: /home/user/.mem7
```
flux7-mesh discovers the tools via `tools/list` ; no per-tool wiring is required. Grants and policies apply as usual.
### Provenance and agent scopes
When mem7 sits behind flux7-mesh as a `streamable-http` upstream, the mesh puts two things in the `_meta` of each `tools/call`:
- `traceparent`, the W3C trace context of the governed call. mem7 records its trace id on every write and every deletion, in the markdown (`trace:` in the envelope) and in the index; `memory_recall`, `memory_search` and `memory_context` return it. A memory points back to the decision that produced it.
- `art.flux7/agent`, the agent the mesh authenticated, for an upstream configured with `forward_identity: true`. mem7 honours it **only on a request that carried its bearer token**: anyone can write a `_meta`, only the mesh holds the token. The vouched identity replaces the `agent` argument the caller declared.
```yaml
# flux7-mesh
memory:
url: http://localhost:9070
token: "${MEM7_TOKEN}" # expanded from the service environment
mcp_servers:
- name: memory
transport: streamable-http
url: http://localhost:9070/mcp
forward_identity: true
headers:
Authorization: "Bearer ${MEM7_TOKEN}"
```
With a scopes file, an identified agent reads its own memories plus the owners listed for it; only administrators overwrite or forget another agent's memory, forget by tags, or read the raw workspace (`memory_get`):
```json
{ "read": { "claude": ["*"], "sup7": ["scout7"] }, "admin": ["claude"] }
```
Clients that reach mem7 directly with the token but name no agent (the supervisor, the console, the mesh's own decision writer) are not scoped. Without a scopes file, identities still sign the writes and reads stay open. `contrib/systemd/enable-token.sh` wires the token and the scopes into the systemd units of a single machine.
To share the same memory across several machines behind flux7-mesh, run `mem7 serve` on one host and point the other hosts at it via the upcoming remote-client mode (Phase 1.5 of the roadmap).
### Time: valid and as-of
Every memory is a series of versions on two axes: when it holds in the world (`valid_from`, `valid_to`) and when mem7 believed it (`tx_from`, `tx_to`). `memory_store` takes optional `valid_from` / `valid_to` (a date `2026-03-20` or RFC3339); a new version ends the ones it overlaps and keeps what they said outside its period, so a value given "from now" ends the old one now, and a date in the past corrects history without erasing what was believed. Reads take `valid_at` and `as_of`; without them they return what mem7 believes now of what holds now, as before. `memory_forget` stops believing a key, and a read `as_of` an earlier moment still sees it. An index built before versions is rebuilt from the markdown at start. See [Time](https://docs.flux7.art/mem7/time/).
### Hash chain (tamper evidence)
Every entry mem7 writes to the workspace (store, deletion, deletion by tags) carries the seal of the entry before it (`prev:`) and its own (`hash:`), computed over its parsed fields. Edit an entry, drop one or reorder them, and `mem7 verify` names the first place the chain no longer holds:
```
$ MEM7_CHAIN_KEY=... mem7 verify
211 entries: 78 sealed, 133 written before the chain
seals: HMAC-SHA256 with MEM7_CHAIN_KEY
chain holds
```
With `MEM7_CHAIN_KEY` the seal is an HMAC-SHA256: editing the workspace without the key leaves a break no one can reseal. Without it, a plain SHA-256 catches accidents and careless edits, not a forger. The key changes only through `mem7 rekey` (below): changed in the environment alone, the next entry breaks the chain. Entries written before the chain existed are counted, not checked; the chain starts at the first sealed entry. The workspace stays plain markdown you can read and edit by hand; an edit now shows.
#### Rotating the key
```
$ systemctl stop mem7
$ MEM7_CHAIN_KEY=<current> MEM7_NEW_CHAIN_KEY=<new> mem7 rekey --agent <who>
chain handed to key 402a8acd4a904b1a (425 entries sealed, 1 rotation(s))
```
`rekey` checks that the chain holds, then appends a rekey entry sealed with the new key over the last seal of the old one. Set `MEM7_CHAIN_KEY` to the new key, add the old one to `MEM7_CHAIN_KEYS_RETIRED`, and start the daemon. A holder of the retired key can no longer reseal the entries before the rotation (the rekey entry would not follow them) nor append after it, and `verify` requires the chain to end under the current key. Stop the daemon first: it keeps the chain head in memory. `rekey` also moves a keyless chain (SHA-256) to a key.
## Tools
### memory_store
Upsert a memory entry by key. The markdown workspace receives an append-only section ; the SQLite index is updated in place. If hybrid search is enabled, an embedding is computed and stored alongside the entry.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `valid_from` | string | no | When the fact starts to hold (date or RFC3339); default now; in the past, it corrects history |
| `valid_to` | string | no | When it stops holding; default open |
| `key` | string | yes | Unique key for this memory |
| `value` | string | yes | Content to remember (free-form markdown allowed) |
| `tags` | string[] | no | Tags for filtering and grouping |
| `agent` | string | no | Identifier of the storing agent |
| `ttl` | number | no | Time-to-live in seconds (0 = permanent) |
### memory_recall
Recall memories by key, tags, or agent, most recently updated first. Bumps `access_count` and `last_accessed` on returned entries.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `valid_at` | string | no | What held at that moment (date or RFC3339); default now |
| `as_of` | string | no | What mem7 believed at that moment; default now |
| `key` | string | no | Exact key to recall |
| `tags` | string[] | no | Filter by tags (AND logic) |
| `agent` | string | no | Filter by agent |
| `limit` | number | no | Max results (default 10) |
### memory_search
Full-text search over memories using SQLite FTS5, ranked by field-weighted BM25. When hybrid search is enabled, results are merged with dense cosine similarity via RRF. Supports FTS5 operators in raw mode : `foo*` prefix, `AND` / `OR` / `NOT`, quoted phrases.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `valid_at` | string | no | What held at that moment (date or RFC3339); default now |
| `as_of` | string | no | What mem7 believed at that moment; default now |
| `query` | string | yes | Search query |
| `mode` | string | no | `raw` (default, FTS5 syntax) or `natural` (plain language, auto-stemmed) |
| `tags` | string[] | no | Post-filter by tags |
| `agent` | string | no | Post-filter by agent |
| `since` | string | no | Lower bound on `updated_at` (RFC3339) |
| `until` | string | no | Upper bound on `updated_at` (RFC3339) |
| `limit` | number | no | Max results (default 10) |
| `include_neighbors` | boolean | no | Fetch sequential neighbors around matching entries (default false) |
| `neighbor_radius` | number | no | How many neighbors to fetch on each side (default 1) |
### memory_context
Same search capabilities as `memory_search` but returns a JSON array of structured objects instead of formatted markdown. Designed for programmatic use by agent SDKs.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `valid_at` | string | no | What held at that moment (date or RFC3339); default now |
| `as_of` | string | no | What mem7 believed at that moment; default now |
| `query` | string | yes | Search query |
| `mode` | string | no | `raw` (default) or `natural` |
| `tags` | string[] | no | Post-filter by tags |
| `agent` | string | no | Post-filter by agent |
| `since` | string | no | Lower bound on `updated_at` (RFC3339) |
| `until` | string | no | Upper bound on `updated_at` (RFC3339) |
| `limit` | number | no | Max results (default 10) |
| `include_neighbors` | boolean | no | Fetch sequential neighbors (default false) |
| `neighbor_radius` | number | no | Neighbors on each side (default 1) |
Returns a JSON array of `{ "key", "value", "tags", "agent", "updated" }` objects.
### memory_get
Read a file from the markdown workspace, optionally between `from_line` and `to_line` (1-indexed, inclusive). Paths are resolved relative to the workspace root and refused if they escape it.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | yes | Workspace-relative path (e.g. `memory/2026-04-11.md`) |
| `from_line` | number | no | First line to read |
| `to_line` | number | no | Last line to read |
### memory_list
List memory keys with metadata (without values).
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `valid_at` | string | no | What held at that moment (date or RFC3339); default now |
| `as_of` | string | no | What mem7 believed at that moment; default now |
| `tags` | string[] | no | Filter by tags |
| `agent` | string | no | Filter by agent |
### memory_forget
Delete memories by key and/or tags. A tombstone section is appended to the markdown workspace, and the SQLite index soft-deletes the matching rows.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `key` | string | no | Exact key to delete |
| `tags` | string[] | no | Delete all entries matching these tags (AND logic) |
| `agent` | string | no | Recorded on the tombstone |
### memory_history
The life of one key, oldest first, read from the markdown workspace (the index only knows the current state): every store, update and deletion, by key or by tags, with its author, the trace id of the governed call behind it, and its seal in the hash chain. Entries written before the chain show as unsealed. Scoped like a read.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `key` | string | yes | Exact key |
## HTTP endpoints
`mem7 serve` exposes these routes :
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/healthz` | Liveness probe — returns `{"status":"ok","version":"..."}` (always public, no auth) |
| `POST` | `/rpc` | JSON-RPC 2.0 endpoint — same MCP tool surface as stdio |
| `POST` | `/memory/snapshot_reminder` | Returns a structured instructional payload for an agent runtime to inject into its context before compaction |
| `GET` | `/memory/chain` | The workspace's hash chain report, as `mem7 verify` prints it: `{"holds", "report": {entries, sealed, legacy, keyed, break}}` (bearer auth) |
Bearer auth is applied to `/rpc`, `/mcp`, `/sse`, `/messages` and `/memory/*` when `MEM7_TOKEN` (or `--token`) is set.
Example :
```bash
curl -s -X POST http://localhost:9070/rpc \
-H "Authorization: Bearer $MEM7_TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"memory_search","arguments":{"query":"roadmap*"}}}'
```
## Architecture
```
Claude Code / flux7-mesh / Python SDK / scripts
│
MCP stdio ┴ HTTP JSON-RPC
│
┌─────▼─────┐
│ Dispatcher │ ← MCP protocol layer
└─────┬─────┘
│
┌─────▼─────┐
│ Store │ ← orchestrator
└──┬──┬──┬──┬┘
│ │ │ │
┌──────▼┐ │ ┌▼──────────┐ ┌▼─────────┐
│markdown│ │ │ sqlite │ │ reranker │
│workspace│ │ │ (facts + │ │ (Ollama) │
│(truth) │ │ │ FTS5 + │ │ opt-in │
└────────┘ │ │ embeds) │ └───────────┘
│ └───────────┘
┌──────▼──────┐
│ embedder │ ← opt-in, external
│ (Ollama / │
│ OpenAI) │
└─────────────┘
```
Every write goes through the markdown writer first and then updates the SQLite index. If hybrid search is enabled, an embedding is computed via the external provider and stored as a BLOB. Reads consult the index only ; embeddings are cached in memory for sub-ms cosine search. If the index is corrupted or out of sync, `mem7 rescan` drops it and replays the markdown chronologically to reconstruct a consistent state.
## License
Apache 2.0
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues