patina
by Whale02
README.md
# Patina
**Affect-weighted persistent memory for LLM agents.** Postgres + pgvector, served over MCP.
Your agent forgets everything between sessions. You can dump transcripts into a
vector store and search them, but after a few thousand entries that store
returns something plausible for any query you type. Delete the entries that
look unimportant and you lose the March note that explains everything in
September.
Patina makes forgetting an operation you design. Every memory carries emotional
coordinates and a trace log: when it was written, when it was shown, when the
agent actually used it. A daily worker recomputes its weight from that history,
each trace decaying as a power of its age, so what you keep coming back to
stays close and what you never touch drifts down the list. It stays reachable
by direct search, the way you can't list your neighbor's phone number but
recognize it when you see it.
```
valence ↑
│ tender excited
│ (+0.7, 0.3) (+0.9, 0.85)
│ · ·
─────────┼─────────────────────────────────→ intensity
│ · ·
│ withdrawn alarmed
│ (-0.5, 0.2) (-0.6, 0.85)
↓
weight = importance × affect × Σ w(trace) × age(trace)^(−d)
traces d from kind
affect = 1 + intensity·0.8 + |valence|·0.4
w = 1.0 created, 1.0 used, 0.1 surfaced
```
---
## What you get
Three claims carry the rest.
**Forgetting runs on use.** ACT-R base-level activation over a trace log: every
use is its own decaying trace, so recency and frequency of use both count and
use rejuvenates. Exposure counts a tenth of use, so appearing in a result list
is not what keeps a memory appearing in result lists. Promises hold full weight
until you resolve them. Milestones never decay.
**Affect is a required column that ranks.** `recall(query, mood=-0.6)` biases
results toward memories of matching valence, the way a low mood surfaces
different memories from the same store than a good day does.
**The constants were measured rather than felt.**
`scripts/evaluate_activation.py` scores the model on your own trace log by
temporal holdout, use as the label, against the previous model and naive
baselines. On a synthetic store built with the structure the model targets it
reaches AUC 0.84, where the old exponential model reaches 0.56. That store is
generated, and the number that counts is the one from your traces.
Under those sits the ordinary machinery. Retrieval fuses dense vectors with
trigram search through Reciprocal Rank Fusion, then expands along a typed
memory graph. `recall(query, max_tokens=800)` packs results by rank into a
token budget and tells you what it left out. Near-identical writes collapse
into the original, and a similar-but-evolved one supersedes its predecessor
without destroying it. Deletion archives first, enforced by database grants
rather than by good intentions. Raw session transcripts become searchable
summarized chunks, so the agent can recall a night nobody wrote down. Three
synthesized persona dimensions hold what the agent is, what it understands
about you, and what the relationship has become.
Every client reads the same rows: claude.ai, Claude Code, Claude Desktop, your
own agent loop. There is nothing to sync.
Most people will point this at an assistant that needs to remember a project.
It was built for the harder case, one person modeled over years, which is why
supersession and affect and the persona layer look over-built for a notes file.
[docs/DIGITAL_TWIN.md](docs/DIGITAL_TWIN.md) covers that end.
---
## Quick start
You need Docker and [Ollama](https://ollama.com) for local embeddings. (To
try the system without a model first, set `EMBED_PROVIDER=hash` and
`PATINA_ALLOW_HASH_EMBEDDINGS=1`; it is deterministic and offline, and it
matches spelling, not meaning. Switch to Ollama before you store anything real.)
```bash
git clone https://github.com/Whale02/patina.git
cd patina
ollama pull bge-m3
cp .env.example .env
chmod 600 .env
# Fill in three secrets:
# POSTGRES_PASSWORD = $(openssl rand -hex 32)
# POSTGRES_APP_PASSWORD = $(openssl rand -hex 32)
# MCP_BEARER_TOKEN = $(openssl rand -hex 32)
docker compose up -d
curl http://localhost:8000/health # {"status":"ok"}
docker compose exec -T server python -m scripts.smoke_test
```
Give the agent a persona to wake up as:
```bash
cp persona/agent.example.md persona/agent.md
cp persona/user.example.md persona/user.md
cp persona/shared.example.md persona/shared.md
$EDITOR persona/*.md
```
The server refuses to start when a required variable is missing. Nothing
security-relevant falls back to a default.
### Connecting a client
**Claude Code.** Add to `.mcp.json`:
```json
{
"mcpServers": {
"patina": {
"type": "http",
"url": "http://localhost:8000/mcp/",
"headers": { "Authorization": "Bearer YOUR_MCP_BEARER_TOKEN" }
}
}
}
```
**claude.ai and Claude Desktop** need a public HTTPS URL and the OAuth flow.
See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md).
In a session:
```
> wake()
# → pinned memories, unresolved promises, all three persona dimensions
> remember("Decided to leave the job. Terrified and certain at the same time.",
valence=0.2, intensity=0.85, importance=9, kind="milestone")
> recall("how did I feel about leaving")
> cite(["<id of the memory you drew on>"])
# use is what keeps a memory available; recall() alone only logs exposure
```
---
## The verbs
| Tool | What it does |
|---|---|
| `wake` | Conversation start: default surface plus all three persona dimensions. |
| `recall` | Recall. No args gives pinned and unresolved. `query=` runs hybrid search with graph expansion. |
| `remember` | Write a memory. Affect coordinates required. Deduplicates on write. |
| `digest` | Digest long text into 2-6 discrete memories. |
| `revise` | Resolve, pin, supersede, retag, or delete (archived first). |
| `cite` | Report which recalled memories you actually used. The signal that separates memories that matter from memories that keep appearing. |
| `link` / `unlink` / `graph` | Typed edges between memories. |
| `status` | Counts, edges, health flags. |
| `forget` | Recompute every weight from the trace log now. Idempotent. |
| `find_duplicates` / `find_fading` | Find duplicates. Find memories about to fade. |
| `persona_write` / `persona_regen` | Read and propose updates to the self-model. |
| `monthly_summary` / `tag_index` | Rollups written to the markdown mirror. |
| `propose_canonical_tag` | Suggest a tag for the closed vocabulary. |
Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md).
The names are the verbs an agent already thinks in. It wakes, recalls,
remembers, digests, revises, forgets. Read the tool list and it reads as a
description of what a memory does.
---
## Documentation
| | |
|---|---|
| [docs/MEMORY_MODEL.md](docs/MEMORY_MODEL.md) | Affect, decay, and how forgetting works. Start here. |
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | How a write and a read flow through the system. |
| [docs/TOOLS.md](docs/TOOLS.md) | Every tool, every parameter. |
| [docs/PERSONA.md](docs/PERSONA.md) | The three-dimensional self-model. |
| [docs/DIGITAL_TWIN.md](docs/DIGITAL_TWIN.md) | The deep end: modeling one person over years. |
| [docs/EMBEDDINGS.md](docs/EMBEDDINGS.md) | Providers, dimensions, migrating between them. |
| [docs/SECURITY.md](docs/SECURITY.md) | Threat model, auth, and the gaps I left open. |
| [docs/PRIVACY.md](docs/PRIVACY.md) | What leaves your machine, and how to stop it. |
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Public exposure, OAuth enrollment, backups, scheduling. |
---
## Scale
Patina runs one agent for one person, across thousands to tens of thousands of
memories. At that size it holds up: SHA-pinned container digests, a
least-privileged database role, audit logging, rate limiting, 125 tests, and
CI that loads the real schema, checks the privilege model, applies every
migration twice, and runs the database smoke suite as the app role.
It does not run at production scale, and you should know where it breaks before
you find out:
- **No vector index.** pgvector's ivfflat and hnsw cap at 2000 dimensions, and
brute-force cosine over 10k rows takes about 10ms. Past roughly 50k memories
you want to truncate to 1536 dims and add HNSW. `patina/schema.sql` describes
the migration.
- **OAuth tokens live in process memory.** A restart invalidates them and
clients re-authenticate without noticing. Serving more than one person would
need a table.
- **The trace log grows without bound, by design.** One row per memory per
hour it is surfaced, one per use, one per write. A busy single-person store
adds a few hundred thousand rows a year, and the nightly recompute scans all
of them (an indexed aggregate; seconds at that size). Past tens of millions
of rows you would want to roll old traces up into a per-memory summary. The
formula supports it, since a trace's contribution depends only on its age
and kind, and nobody has needed it yet.
- **The constants are measured on synthetic data, not yet on yours.** The
activation harness gives `d`, the exposure weight, and the retrieval prior a
number on a generated store; a generated store is not a person's memory. The
holdout on real traces is the measurement that counts, and every store starts
with none. Run it after a month of `cite()` traffic and send what you find;
[CONTRIBUTING.md](CONTRIBUTING.md) says how.
- **The use signal depends on the agent calling `cite()`.** Without it the
model degrades gracefully toward exposure-and-age ordering, and throws away
its best evidence. Watch the `used` count in `status()`.
If you need multi-tenant agent memory at scale, the ideas here will help you
more than the code will.
---
## Contributing
Retrieval evaluation, more embedding providers, and transcript ingestion for
platforms beyond Claude Code are the areas where help goes furthest. See
[CONTRIBUTING.md](CONTRIBUTING.md).
## License
MIT. See [LICENSE](LICENSE).
The design leans on the memory literature instead of inventing its own:
Ebbinghaus (1885) for the forgetting curve, Russell (1980) for the affect axes,
Bjork and Bjork (1992) for the storage-versus-retrieval-strength split behind
"fade, don't delete", Anderson and Schooler (1991) for ranking by use, Bower
(1981) for mood-congruent recall, Cormack, Clarke, and Buettcher (2009) for rank
fusion. [docs/MEMORY_MODEL.md](docs/MEMORY_MODEL.md) works through the
derivation and carries the full reference list.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing