Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vta-agent-memory",
"description": "Durable agent memory stored in your own Verifiable Trust Agent, not in the tool. Save and recall facts across sessions, scoped to a VTA trust context you control and can revoke.",
"version": "0.5.0",
"version": "0.6.0",
"keywords": [
"memory",
"vta",
Expand Down
13 changes: 12 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ src/
record.rs the record shape, key encoding, and recall ranking
authorship.rs signing records on save, verifying author + location on read
provenance.rs basis / confidence / evidence: vocabulary, checks, rank weight
server.rs the six MCP tools
lifecycle.rs status, validUntil/reviewAfter, history keys, standing
server.rs the nine MCP tools
tests/
memory_roundtrip.rs Store end-to-end against an in-process fake VTA
mcp_starts_without_a_vta.rs the real binary, over stdio, with no VTA
Expand Down Expand Up @@ -162,6 +163,16 @@ unattributed; and the resolver is `did:key`-only, so a stored record can never
make recall fetch a URL. The signer is loaded from the session store **after**
connecting — the first connect of an enrolled identity rotates its key.

**A save never overwrites blind.** `Store::save_versioned` reads, archives the
current version to `archive/<key>/v<n>`, then writes with `expectedVersion` =
the version it read; a `versionConflict` is a `SaveOutcome::Conflict` carrying the
(checked) winner, never an error to retry. Do not "fix" a conflict by re-saving
unconditionally — that is the lost update this exists to prevent. History keys
must stay outside every `<type>/` prefix, and `forget` must delete them: "forget"
has to mean forget. A status change is a new version signed by whoever made it;
only `retract` is allowed on a refused record, because anything else would
re-sign content nobody vouches for.

## The retrieval constraint

`vta/memory/list/0.2` pages a context in ascending key order and can narrow by
Expand Down
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 = "vta-agent-memory"
version = "0.5.0"
version = "0.6.0"
edition = "2024"
rust-version = "1.95.0"
description = "Agentic memory for Claude Code, stored in a Verifiable Trust Agent"
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,19 @@ downgraded. Verification is `did:key`-only and does no network I/O, so a stored
record cannot make recall fetch anything. `doctor` shows the DID this machine
signs as.

**Nothing a save replaces is lost, and two writers cannot silently overwrite
each other.** A save reads the current version, copies it verbatim — still signed
by its own author — to `archive/<type>/<slug>/v<n>` (outside every type prefix, so
recall never sees it; at most five per memory), and then writes **conditionally**
on the version it read. A writer that got in first wins, and the losing save
comes back as `saved: false` with the winner, not as an overwrite. A caller that
read a memory earlier passes that `version` as `expectedVersion`. Memories also
have a lifecycle — `active`, `deprecated`, `disputed` (both recalled lower, with a
reason) or `retracted` (a tombstone that never recalls) — plus `validUntil`
(recall drops it after) and `reviewAfter` (recall flags it `stale`). Retract is
the "this was wrong" path; `forget` is still the hard delete, and takes the
history with it.

**Memory is not application state.** "Forget everything" has to stay a safe thing
for a user to ask, which it stops being the moment account state lives here. The
VTA's `vta/app-state/*/1.0` family — versioned, namespaced, with a change feed —
Expand Down Expand Up @@ -319,7 +332,7 @@ An entry created before this release grants `any`, under which
The plugin does not disappear. The MCP server starts without contacting the VTA
and connects on the first memory call, so:

- The six tools are always present, and a failure comes back as a tool error the
- The nine tools are always present, and a failure comes back as a tool error the
model can relay — "run `vta-agent-memory setup`" rather than silence.
- `memory_context` answers *without* connecting, so you can still ask which VTA
and context you are pointed at while it is down.
Expand All @@ -337,7 +350,7 @@ cargo test
```

`tests/mcp_starts_without_a_vta.rs` drives the real binary over stdio with no
config at all, asserting it completes an MCP handshake, offers all six tools, and
config at all, asserting it completes an MCP handshake, offers all nine tools, and
returns an actionable error from a tool call. That is a property about process
lifetime, so it can only be observed by running the thing.

Expand Down
4 changes: 3 additions & 1 deletion commands/forget.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ argument-hint: [memory key or description]

Forget this memory: $ARGUMENTS

This is permanent — there is no undo and no grace window.
This is permanent — the memory and its history are deleted, with no undo and no
grace window. If the user wants it marked wrong rather than gone, offer
`memory_retract` instead: it stops the memory recalling and keeps a record of why.

If the argument is already a key (`type/name`), confirm what it is with
`memory_get` and show the user before deleting. If it is a description, use
Expand Down
10 changes: 10 additions & 0 deletions commands/memories.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
description: Show everything in your VTA-backed memory
argument-hint: [--stale]
---

Show the user what is in their VTA-backed memory.
Expand All @@ -12,5 +13,14 @@ bodies.
set there are more: say how many in all, and fetch further pages with `offset`
only if the user wants to see them.

Show each memory's `status` when it is not `active`, and mark ones that are
`stale` or `expired`.

If the arguments include `--stale`, call `memory_list` with `stale: true`
instead, and for each memory it returns — due for review, or past its
`validUntil` — ask the user whether it still holds. Then, per their answer: save
it again (optionally with a new `reviewAfter`), `memory_deprecate` it, or
`memory_retract` it. Do not change any of them without asking.

If the list is empty, say which context you looked in, since an empty result
usually means the wrong context rather than no memories.
45 changes: 38 additions & 7 deletions skills/agent-memory/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ description: How to use the user's VTA-backed memory — what is worth saving, w
# Agent memory (VTA-backed)

The user's durable memory lives in their own Verifiable Trust Agent, not in this
tool and not in a file in the repo. You reach it through six MCP tools:
`memory_recall`, `memory_get`, `memory_save`, `memory_forget`, `memory_list`,
`memory_context`.
tool and not in a file in the repo. You reach it through nine MCP tools:
`memory_recall`, `memory_get`, `memory_save`, `memory_list`, `memory_context`,
the status changes `memory_dispute`, `memory_deprecate` and `memory_retract`, and
the hard delete `memory_forget`.

The whole store is scoped to one **VTA trust context**. That context is the
isolation boundary — an agent scoped to a different one cannot read, write, or
Expand Down Expand Up @@ -179,16 +180,46 @@ Recall first. If a memory already covers it, update that one — `memory_save`
with the same name replaces it — rather than creating a near-duplicate. Two
memories that disagree are worse than neither.

When you update a memory you read, pass its `version` as `expectedVersion`. If
another device or agent changed it since, the save is **not** written and you get
`saved: false` with what is there now (`current`). Do not blindly save again:
read it, merge if your change still makes sense, and save with the new
`currentVersion` — or tell the user it changed under you. Nothing a save
replaces is lost; the previous version goes to the memory's history
(`memory_get` with `history: true`).

If a fact has a shelf life — "on leave until the 1st", "using the staging
cluster this sprint" — give it a `validUntil` date and recall will stop returning
it after that. If it is a preference that may drift, give it a `reviewAfter`
date; recall will then flag it `stale`, and you should confirm it with the user
before relying on it.

Write the description as the answer to: *would I want this loaded at the start of
a session?* If the honest answer is no, do not save it.

## When a memory is wrong or out of date

Prefer marking it to deleting it. Each takes a one-line `reason`, which is
shown with the memory from then on:

| Tool | When | Effect |
|---|---|---|
| `memory_dispute` | Somebody says it is wrong, but it is not settled. | Still recalls, ranked lower, shown as `disputed`. |
| `memory_deprecate` | It was true, it is not any more, or another memory replaced it. | Still recalls, ranked lower, shown as `deprecated`. |
| `memory_retract` | It is wrong, or should never have been saved. | Never recalls again. A tombstone says it existed and why it was withdrawn. |

Saving the memory again settles a dispute: the new version is active. A status
change is a new version signed by you, so the memory's `author` becomes you; the
original author's version is in its history.

## Forgetting

`memory_forget` is permanent. There is no undo and no grace window.
`memory_forget` is permanent: the memory *and its history* are deleted. There is
no undo and no grace window.

Only forget when the user asks, or when you have just saved a memory that
supersedes the old one. If a memory turns out to be wrong, forgetting it is
right — a wrong memory is worse than a missing one.
Only forget when the user asks for something to be gone — not merely marked
wrong. If a memory turns out to be wrong, `memory_retract` it; that keeps the
fact that it was once believed, and why it was withdrawn.

If the user asks you to forget *everything*, confirm the scope first, list what
is there with `memory_list`, and only then delete. `memory_list` returns one page
Expand Down
2 changes: 1 addition & 1 deletion src/lazy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Claude Code starts an MCP server when a session starts and takes what it
//! gets. A server that exits before speaking MCP does not appear as a broken
//! memory service — it does not appear at all: the six tools are simply absent,
//! memory service — it does not appear at all: the memory tools are simply absent,
//! and the model has no way to tell anyone why, because the thing it would use
//! to say so is the thing that is missing.
//!
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod config;
pub mod enrol;
pub mod fence;
pub mod lazy;
pub mod lifecycle;
pub mod pnm;
pub mod provenance;
pub mod record;
Expand Down
Loading
Loading