patina

patina

Affect-weighted persistent memory for LLM agents, served over MCP, enabling emotional memory coordinates, trace-based forgetting, hybrid search, and persona-based recall.

Category
访问服务器

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.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 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.

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

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选