patina
Affect-weighted persistent memory for LLM agents, served over MCP, enabling emotional memory coordinates, trace-based forgetting, hybrid search, and persona-based recall.
README
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 covers that end.
Quick start
You need Docker and Ollama 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.)
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:
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:
{
"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.
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.
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 | Affect, decay, and how forgetting works. Start here. |
| docs/ARCHITECTURE.md | How a write and a read flow through the system. |
| docs/TOOLS.md | Every tool, every parameter. |
| docs/PERSONA.md | The three-dimensional self-model. |
| docs/DIGITAL_TWIN.md | The deep end: modeling one person over years. |
| docs/EMBEDDINGS.md | Providers, dimensions, migrating between them. |
| docs/SECURITY.md | Threat model, auth, and the gaps I left open. |
| docs/PRIVACY.md | What leaves your machine, and how to stop it. |
| 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.sqldescribes 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 ofcite()traffic and send what you find; 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 theusedcount instatus().
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.
License
MIT. See 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 works through the derivation and carries the full reference list.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。