Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2951791
test: extract the live-storage fixture into src/test_support
calebevans-ab Aug 11, 2026
34bdb33
feat: strict argument parsing for the MCP listing tools
calebevans-ab Aug 11, 2026
6cec873
feat: add list_namespaces and stop reporting every namespace as empty
calebevans-ab Aug 11, 2026
d3de3e6
feat: add a list_tags tool for discovering the tag vocabulary
calebevans-ab Aug 11, 2026
727f1f4
fix: stop counting deleted memories in namespace statistics
calebevans-ab Aug 11, 2026
d634442
fix: sort the health report's "top tags" by count, and document the n…
calebevans-ab Aug 11, 2026
93a8ef3
feat: detect near-duplicate memories against the vector index
calebevans-ab Aug 11, 2026
b7f7e32
feat: warn when a stored memory closely resembles an existing one
calebevans-ab Aug 11, 2026
dd1ed03
feat: teach the store tools to report and recover from near-duplicates
calebevans-ab Aug 11, 2026
315f758
feat: report near-duplicates on the HTTP create endpoints too
calebevans-ab Aug 11, 2026
8ac012e
docs: document the near-duplicate advisory
calebevans-ab Aug 11, 2026
a9385ed
docs: stop calling forget_memory a permanent delete
calebevans-ab Aug 11, 2026
92b03ad
feat: add the storage primitives for destroying a namespace
calebevans-ab Aug 11, 2026
550b5b6
feat: let the FTS index drop a whole namespace
calebevans-ab Aug 11, 2026
8c558d1
refactor: split a non-bridging remove_memory out of the graph
calebevans-ab Aug 11, 2026
7970978
refactor: one implementation of "remove this memory from the indexes"
calebevans-ab Aug 11, 2026
53d9e56
fix: the HTTP delete left the memory in every index
calebevans-ab Aug 11, 2026
04b212b
feat: add a forget_memories tool for batch deletion
calebevans-ab Aug 11, 2026
05b4e22
feat: add a delete_namespace tool
calebevans-ab Aug 11, 2026
5151d13
docs: document the two new tools and raise the count to 14
calebevans-ab Aug 11, 2026
03ce1d4
fix: a namespace name could point remove_dir_all at any directory
calebevans-ab Aug 11, 2026
9bebb5e
fix: a case-insensitive filesystem defeated the namespace guards
calebevans-ab Aug 11, 2026
69e59d6
refactor: delete MetadataStore::update_namespace
calebevans-ab Aug 11, 2026
c175d2f
fix: a purge that failed part-way reported a plain failure
calebevans-ab Aug 11, 2026
2f876e4
fix: the non-force delete guard read a different table than the delete
calebevans-ab Aug 11, 2026
5dbeea5
fix: destroying a memory left its peers counting edges that were gone
calebevans-ab Aug 11, 2026
91d4f81
fix: quarantine orphaned namespace directories instead of deleting them
calebevans-ab Aug 11, 2026
7972e69
docs: the FTS namespace-removal guard was documented and tested wrongly
calebevans-ab Aug 11, 2026
caa62b4
fix: the near-duplicate phase guarantee was inert in production
calebevans-ab Aug 11, 2026
eb83712
fix: recall_memories and find_similar_memories still guessed at argum…
calebevans-ab Aug 11, 2026
7494477
fix: an unfilterable entity or tag silently widened the query
calebevans-ab Aug 11, 2026
8cb8200
docs: the #[serde(default)] comments taught the wrong rule
calebevans-ab Aug 11, 2026
2af7951
docs: namespace_stats told the agent the wrong thing about its counts
calebevans-ab Aug 11, 2026
8b45cc4
fix: the batch delete paths trusted what came back and what came in
calebevans-ab Aug 11, 2026
45af7bc
fix: a stale graph id_index entry could never be removed
calebevans-ab Aug 11, 2026
d1cc234
release: v0.2.0
calebevans-ab Aug 11, 2026
001999c
test: the case-variant sweep test destroyed its own fixture on macOS
calebevans-ab Aug 11, 2026
d5a8566
fix: nothing ever loaded vectors.dat into the vector index
calebevans-ab Aug 11, 2026
7e678be
release: v1.0.0
calebevans-ab Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "recalld"
version = "0.1.10"
version = "1.0.0"
edition = "2024"
rust-version = "1.94"
description = "AI memory system"
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ claude mcp add --scope user --transport http recalld http://localhost:7680/mcp

Then allow the MCP tools so Claude can use them without prompting each time. Add to your `~/.claude/settings.local.json` (global) or project `.claude/settings.local.json`:

`delete_namespace` is deliberately left out of the allowlist below. It
destroys every memory in a namespace and removes its vector file from
disk, with no undo and no backup — the one tool worth a confirmation
prompt each time. Add it only if you want that prompt gone.

```json
{
"permissions": {
Expand All @@ -78,10 +83,13 @@ Then allow the MCP tools so Claude can use them without prompting each time. Add
"mcp__recalld__get_memory",
"mcp__recalld__reinforce_memory",
"mcp__recalld__forget_memory",
"mcp__recalld__forget_memories",
"mcp__recalld__find_similar_memories",
"mcp__recalld__create_namespace",
"mcp__recalld__namespace_stats",
"mcp__recalld__list_memories"
"mcp__recalld__list_memories",
"mcp__recalld__list_namespaces",
"mcp__recalld__list_tags"
]
}
}
Expand Down Expand Up @@ -173,7 +181,7 @@ See [docs/benchmark.md](docs/benchmark.md) for full methodology, per-category br

## Usage modes

**MCP server (stdio)** -- Runs as a Model Context Protocol server for AI tools like Claude Code. Exposes 10 tools: `store_memory`, `store_memories`, `recall_memories`, `get_memory`, `reinforce_memory`, `forget_memory`, `find_similar_memories`, `create_namespace`, `namespace_stats`, `list_memories`.
**MCP server (stdio)** -- Runs as a Model Context Protocol server for AI tools like Claude Code. Exposes 14 tools: `store_memory`, `store_memories`, `recall_memories`, `get_memory`, `reinforce_memory`, `forget_memory`, `forget_memories`, `find_similar_memories`, `create_namespace`, `delete_namespace`, `namespace_stats`, `list_memories`, `list_namespaces`, `list_tags`.

```sh
recalld mcp
Expand Down
95 changes: 90 additions & 5 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,9 @@ Shows decay forecast, at-risk memories, and storage breakdown.

## 6. MCP Tools Reference

recalld exposes 10 MCP tools. These are available to any MCP client (Claude Code, Cursor, etc.) when recalld is configured as an MCP server.
recalld exposes 14 MCP tools. These are available to any MCP client (Claude Code, Cursor, etc.) when recalld is configured as an MCP server.

Deletion is worth reading before you use it. `forget_memory` and `forget_memories` do not free anything: a forgotten memory keeps its record and its graph edges for the life of the database, because the decay sweep never reclaims a tombstone. `delete_namespace` is the only operation that does — and it destroys everything else in the namespace along with them, with no undo and no backup.

### `store_memory`

Expand All @@ -764,6 +766,7 @@ Store a new memory.
| `namespace` | string | No | Target namespace (default: `"default"`). |
| `parentId` | string | No | UUID of an existing memory in the same namespace, linked as this memory's parent. The store fails if the target does not exist or is in a different namespace. |
| `supersedes` | string | No | UUID of an existing memory in the same namespace that this one replaces. Recall REMOVES the old memory from results and returns this one in its place. The store fails if the target does not exist or is in a different namespace. |
| `checkDuplicates` | boolean | No | Report existing memories that closely resemble this one (default: `true`). Never blocks the store; it only adds a `nearDuplicates` object to the result. |

#### supersedes semantics

Expand Down Expand Up @@ -795,13 +798,54 @@ always says what actually happened to the link.
- **Counted.** The new memory's `edgeCount` includes the supersedes edge.
Memories stored before this behaviour existed are not backfilled.

#### Near-duplicate warnings

Every store compares the new memory against what is already there. When
something is at least 0.85 cosine similar, the result carries a
`nearDuplicates` object naming up to three existing memories, closest first,
each with its id, score, and summary (truncated to 240 bytes).

- **Advisory, never a block.** The memory is stored either way. The field is
absent — not `null` — when nothing crossed the threshold, when
`checkDuplicates: false` was passed, or when the namespace's vectors are
not L2-normalized.
- **0.85 is the same number `find_similar_memories` scan mode uses**, so the
two surfaces cannot disagree about whether a pair of memories is a
duplicate. It is not configurable: the right value is a property of the
embedding model, not of the deployment.
- **Scope.** Full- and summary-phase memories in the same namespace only —
the same set auto-linking considers. The phase is re-checked against the
stored record when the report is built, so a memory that has decayed or been
deleted since it was indexed is dropped rather than reported with an empty
summary.
- **The recovery.** The warning arrives after the write, so acting on it
means undoing: `forget_memory` on the id you just created, then either
`reinforce_memory` on the existing memory (nothing new to say) or
`store_memory` again with `supersedes` set to the existing id (the fact
changed). Leave both only when they are genuinely distinct facts that
happen to read alike.
- **Cost.** The scan is a linear pass over every vector in the index — all
namespaces, since the index is one flat array with a per-entry namespace
filter — so it scales with the total number of memories, not with the size
of the namespace being written to. Pass `checkDuplicates: false` during
bulk ingestion.
- **Batches catch themselves.** `store_memories` indexes each item before
checking the next, so a batch containing the same fact twice reports the
second against the first.
- **Normalization caveat.** Scores are dot products, which are cosine
similarities only for L2-normalized vectors. In a `passthrough` namespace
the caller supplies the vectors and nothing validates them, so the check
verifies the query vector first and skips rather than reporting a number
that is not a similarity. The skip is logged at `debug` every time and at
`warn` once per process.

### `store_memories`

Store multiple memories in a single call. Each item has the same schema as `store_memory`. Returns an array of results, one per input memory.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `memories` | array | Yes | Array of memory objects (max 100 per call). Each object has the same fields as `store_memory`, including `supersedes` and its preconditions. |
| `memories` | array | Yes | Array of memory objects (max 100 per call). Each object has the same fields as `store_memory`, including `supersedes` and its preconditions, and `checkDuplicates` — both are set per item, and `nearDuplicates` is reported per result entry. |

### `recall_memories`

Expand Down Expand Up @@ -841,11 +885,19 @@ Strengthen a memory so it decays more slowly.

### `forget_memory`

Permanently delete a memory.
Delete a memory's content. The memory moves to the Tombstone phase: its summary, full text and tags are erased and it stops appearing in recall, but its record and its graph edges are kept so relationship chains stay intact, and its UUID is never reused. Tombstoned memories are never reclaimed by the decay sweep, so they persist for the life of the database.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | string | Yes | Memory UUID to forget. The memory moves to the Tombstone phase; its graph edges are kept and its UUID is never reused. |

### `forget_memories`

Delete several memories in one call. Each moves to the Tombstone phase exactly as `forget_memory` describes. Returns one result per input id, in the order given; `deleted: false` means the id was unknown or the memory had already been forgotten.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | string | Yes | Memory UUID to delete. |
| `ids` | string[] | Yes | Memory UUIDs to forget (1-100). Each moves to the Tombstone phase; graph edges are kept and UUIDs are never reused. |

### `find_similar_memories`

Expand Down Expand Up @@ -873,9 +925,24 @@ Create a new memory namespace.
| `desiredRetention` | number | No | Target retention rate 0.0-1.0 (default: 0.9). |
| `decayRateMultiplier` | number | No | Per-namespace decay rate multiplier. 1.0 = normal, 2.0 = 2x slower, 0.0 = disabled. Omit to inherit global setting. |

### `delete_namespace`

**Destructive and irreversible. There is no undo and no backup.** Destroys a namespace and everything in it: every memory, every graph edge touching one, its search index entries, and its vector file on disk.

A namespace that still holds live memories is refused unless you pass `force: true`; the refusal names the count and touches nothing. A namespace holding only tombstones counts as empty and deletes without `force` — the response reports those separately as `tombstonesPurged`.

This is the only operation that reclaims tombstones. The `default` namespace cannot be deleted, with or without `force`, and whether or not it is empty: it is recreated on every startup under the same directory name but with a new namespace id, so deleting it would leave a stale `vectors.dat` that the recreated namespace could silently reuse.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Namespace to delete (1-64 chars). The `default` namespace cannot be deleted. |
| `force` | boolean | No | Delete the namespace even if it still holds memories (default: false). Forced deletion permanently destroys every memory in it and removes its vector file from disk. |

### `namespace_stats`

Get statistics for a memory namespace including total memory count, phase breakdown (full/summary/ghost), permastore count, average strength, edge count, and vector storage size.
Get statistics for a memory namespace including the live memory count, tombstone count, phase breakdown (full/summary/ghost), permastore count, average strength, edge count, and vector storage size.

`memoryCount` counts live memories only and always equals `full + summary + ghost`. Deleted memories appear separately as `tombstoneCount`.

| Parameter | Type | Required | Description |
|---|---|---|---|
Expand All @@ -895,6 +962,24 @@ List memories in a namespace with pagination and optional filters. Unlike `recal
| `timeRangeStart` | integer or string | No | Lower bound: epoch ms (integer) or ISO 8601 string. Only memories created at or after this time are returned. |
| `timeRangeEnd` | integer or string | No | Upper bound: epoch ms (integer) or ISO 8601 string. Only memories created at or before this time are returned. |

A `limit` above the maximum is rejected, not clamped: silently returning 200 rows to a caller who asked for 1000 makes a paginating agent believe it has seen the whole namespace.

### `list_namespaces`

List every namespace with its ID, embedding dimensions, live memory count, and creation date. Call this first when you do not know which namespaces exist. Counts exclude deleted memories.

Takes no parameters.

### `list_tags`

List the tag vocabulary with per-label memory counts, most common first. Use it to reuse an existing tag instead of inventing a near-duplicate. Results come in four buckets — plain `tags`, plus `entities`, `topics`, and `emotions` — with the derived buckets' prefixes stripped so their names can be passed straight back to `store_memory` or `list_memories`. Counts cover live memories only.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `namespace` | string | No | Namespace to count within (default: `"default"`). |
| `allNamespaces` | boolean | No | Count across every namespace (default: `false`). Cannot be combined with an explicit `namespace`. |
| `limit` | integer | No | Maximum labels per bucket (default: 50, max: 500). Each bucket also reports its untruncated total. |

---

## 7. Namespaces
Expand Down
Loading
Loading