memex

io.github.ayushagrawal288由 GitHub 帳號 ayushagrawal288 發布v0.1.0更新於 Oct 24, 2026

Persistent memory for AI agents — semantic + recency search, ONNX embeddings, Docker Compose.

已驗證STDIO僅桌面資料庫知識與記憶

概覽

AI 產生的概覽

自架的 AI 代理持久記憶服務,提供儲存、搜尋、刪除與計數記憶的工具,依語意相似度與時間新近度排序。

可以做什麼
  • 儲存記憶,可指定類型與重要性,於本機產生向量並寫入資料庫
  • 依語意相似度加上時間衰減搜尋記憶,alpha 可調整
  • 依 UUID 刪除指定記憶,並統計某個代理/使用者配對的記憶數量
  • 以本機整套服務執行,附帶 REST API、Prometheus 指標與 Grafana 儀表板
適用情境
適合希望代理跨工作階段記住事實、偏好或過往事件,且要求記憶留在本機而非託管服務的情境。也適合對話型代理,需要近期內容比單純語意相近的舊內容更優先。
執行需求
需要 Docker 與 Docker Compose 在本機執行整套服務;MCP 端點是 8001 埠上的 streamable HTTP 服務,由用戶端連線。不需 API 金鑰,向量由本機 ONNX 模型產生,Postgres 與 pgvector 由 compose 檔啟動。
安裝前請注意
記憶存放在本機 Postgres 資料庫,對話內容會持久化到磁碟,應視為敏感資料處理。刪除工具與背景摘要工作會移除資料:當情境記憶超過門檻時,舊記憶會被壓縮合併並刪除原始紀錄。內附的 Grafana 使用預設管理員密碼,應盡快變更。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 memex,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

memex

[GitHub release] [Python] [FastAPI] [MCP] [License: MIT] [ayushagrawal288/memex MCP server]

[memex MCP server]

A production-grade persistent memory service for AI agents. Agents forget everything between sessions by default — memex fixes that. It stores, retrieves, and ranks conversation memory using semantic search with recency decay, so agents surface what's relevant and recent, not just what's semantically closest.

POST /v1/memories          → store a memory, embed it, persist to PostgresPOST /v1/memories/search   → retrieve top-k memories ranked by similarity + recencyDELETE /v1/memories/{id}   → forget a specific memoryGET  /v1/memories/count    → how many memories does this agent/user haveGET  /health               → liveness + DB connectivity checkGET  /metrics              → Prometheus metrics

Architecture

caller (agent / app)        │        ▼  FastAPI (async)        │   ┌────┴────┐   │         │embeddings  asyncpg pool (min=5, max=20)(fastembed  │ ONNX,      ▼ local)  PostgreSQL 16           pgvector extension           ivfflat index (cosine)

Write path: content → fastembed ONNX inference (local, ~12 ms CPU, BAAI/bge-small-en-v1.5) → INSERT with 384-dim vector → return memory ID.

Read path: query → embed → pgvector cosine search (top_k × 3 candidates) → re-rank with recency decay in Python → return top_k results with scores.


Design decisions

1. Recency decay on top of semantic search

Pure vector similarity returns the most semantically similar memories, not the most useful ones. A fact from 90 days ago that's a 0.95 similarity match is often less useful than a 0.80 match from yesterday.

Score formula:

score = α × cosine_similarity + (1 − α) × exp(−λ × age_days)

Where λ = ln(2) / half_life_days (default: 30 days, so a 30-day-old memory has 50% recency weight).

α is configurable per request (default 0.7). Task-focused agents use higher α (semantic dominates). Conversational agents use lower α (recency matters more).

2. Fetch 3× candidates, re-rank in Python

The pgvector query returns top_k × 3 candidates sorted by pure similarity. Python re-ranks with the decay formula and slices to top_k. This prevents recency decay from starving high-similarity older memories — they're still in the candidate pool.

At 10× scale (>1M memories per agent): push the scoring into a Postgres function using pg_proc to eliminate the Python re-ranking round-trip.

3. asyncpg + explicit pool sizing over SQLAlchemy async

SQLAlchemy adds ORM overhead on every query. The hot retrieval path — embed, query, re-rank — needs to be tight. asyncpg gives direct control over pool min/max (same instinct as tuning HikariCP in Java). pgvector queries require raw SQL for the <=> operator anyway.

Pool defaults: min=5, max=20. Right-size for a single-instance deployment. Override via DB_MAX_POOL_SIZE env var.

4. Rate limiting in Postgres, not Redis

Sliding window counter via upsert. One fewer dependency. Correct under concurrent requests (transactional upsert). At 10× scale with distributed deployments: replace with Redis INCR + EXPIRE — atomic operations, no lock contention.

5. ivfflat index, not HNSW

ivfflat has lower build cost and lower memory footprint — the right tradeoff at small-to-medium scale (<1M vectors). lists=100 works well up to ~1M rows. At 10× scale: switch to HNSW (m=16, ef_construction=64) for better recall at the cost of higher memory and build time.


Running locally

Prerequisites: Docker and Docker Compose. No API keys required — the entire stack runs locally.

bash
git clone https://github.com/ayushagrawal288/memexcd memexdocker compose up

The API is live at http://localhost:8000. Interactive docs at http://localhost:8000/docs.


API reference

Store a memory

bash
curl -X POST http://localhost:8000/v1/memories \  -H "Content-Type: application/json" \  -d '{    "agent_id": "my-agent",    "user_id": "user-123",    "content": "User prefers concise responses and dislikes verbose explanations.",    "memory_type": "semantic",    "importance": 1.2  }'
json
{  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",  "agent_id": "my-agent",  "user_id": "user-123",  "content": "User prefers concise responses and dislikes verbose explanations.",  "importance": 1.2,  "memory_type": "semantic",  "created_at": "2026-05-26T10:30:00Z",  "score": null}

Search memories

bash
curl -X POST http://localhost:8000/v1/memories/search \  -H "Content-Type: application/json" \  -d '{    "agent_id": "my-agent",    "user_id": "user-123",    "query": "how does this user like to communicate",    "top_k": 5,    "alpha": 0.7  }'
json
{  "results": [    {      "id": "3fa85f64-...",      "content": "User prefers concise responses and dislikes verbose explanations.",      "memory_type": "semantic",      "created_at": "2026-05-26T10:30:00Z",      "score": 0.8921    }  ],  "query": "how does this user like to communicate",  "total": 1}

Memory types

TypeUse for
episodicSpecific events, past conversations
semanticFacts, preferences, general knowledge
proceduralWorkflows, how-to instructions

Load test results

Run on a MacBook M-series, Docker Desktop, single Postgres instance:

bash
locust -f scripts/load_test.py --host=http://localhost:8000 \       --headless -u 50 -r 10 -t 60s

Realistic load (50 users, 100–300 ms think time — models actual agent traffic):

EndpointRPSp50 (ms)p95 (ms)p99 (ms)Error rate
POST /v1/memories (write)271602703300%
POST /v1/memories/search831102002500%
Aggregated1131202303000%

Saturation test (500 users, minimal think time — finds the throughput ceiling):

EndpointRPS (plateau)p50 (ms)p99 (ms)Error rate
POST /v1/memories (write)283,9006,1000%
POST /v1/memories/search913,6005,8000%
Aggregated~1203,7005,9000%

Run on MacBook M-series, Docker Desktop (4 CPUs), 4 uvicorn workers, 16 threads/worker.
Embeddings: local ONNX (BAAI/bge-small-en-v1.5) — zero external API calls, zero cost.

Why the ceiling is ~120 RPS:
Every write and every search requires one ONNX inference (~10–15 ms on CPU). With 4 Docker CPUs: 4 cores / 12 ms ≈ 333 embeddings/s theoretical max. After Python overhead, DB queries, and asyncio scheduling: ~120 RPS actual.

Path to higher throughput:

ApproachExpected gainComplexity
Embedding cache (Redis, key = SHA256 of text)2–3× (40–60% hit rate on repeated agent queries)Low
Horizontal scaling (N replicas behind a load balancer)N× linearMedium
GPU inference (swap ONNX runtime → CUDA)10–50×Medium
Voyage-3 API (offload to Anthropic's inference fleet)Scales to thousands of RPS, limited by API quotaLow code change

Project structure

memex/├── app/│   ├── main.py                  # REST API — FastAPI, lifespan, router registration│   ├── mcp_server.py            # MCP server — single-worker FastAPI on port 8001│   ├── core/│   │   └── config.py            # All settings, loaded from env│   ├── db/│   │   └── pool.py              # asyncpg pool, migrations│   ├── models/│   │   └── schemas.py           # Pydantic request/response models│   ├── services/│   │   ├── embeddings.py        # fastembed ONNX inference (local, zero API calls)│   │   ├── local_summarizer.py  # Extractive summariser — Jaccard dedup + TF scoring│   │   ├── memory.py            # Core write/search/scoring logic│   │   ├── metrics.py           # Prometheus metric definitions│   │   ├── summarizer.py        # Background summarisation job│   │   └── rate_limit.py        # Sliding window rate limiter│   └── api/routes/│       ├── memories.py          # Memory endpoints│       ├── health.py            # Health + readiness│       └── mcp_tools.py         # MCP tool definitions (store, search, delete, count)├── scripts/│   └── load_test.py             # Locust load test├── docker-compose.yml├── Dockerfile└── requirements.txt

Observability

docker compose up starts Prometheus and Grafana alongside the API:

ServiceURLCredentials
REST API docshttp://localhost:8000/docs—
MCP serverhttp://localhost:8001/mcp/—
Prometheushttp://localhost:9090—
Grafanahttp://localhost:3000admin / admin

The Grafana dashboard is provisioned automatically. Panels:

  • HTTP request rate + latency p50/p99 — from prometheus-fastapi-instrumentator
  • Embedding API latency p50/p99 — per-attempt histogram by operation (embed / embed_batch)
  • Memory operations/s — create, search, delete throughput
  • DB pool utilisation — active vs idle connections (update interval: 15 s)
  • Summariser activity — memories condensed per hour, run outcomes
  • Embedding errors/min — by operation and error type

Custom metrics are in app/services/metrics.py and exposed on /metrics alongside the standard FastAPI instrumentator metrics.


MCP endpoint

memex exposes itself as an MCP server so any MCP-aware agent (Claude Desktop, Claude Code, custom agents) can store and retrieve memories without custom HTTP integration.

Transport: Streamable HTTP (MCP 2024-11-05 spec). Single-worker process on port 8001 — session state is in-process, so a separate service avoids sticky-session complexity while keeping the REST API's multi-worker throughput.

Tools:

ToolDescription
store_memoryEmbed + persist a memory (type, importance configurable)
search_memoriesSemantic + recency ranked retrieval with configurable alpha
delete_memoryForget a specific memory by UUID
count_memoriesHow many memories an agent/user pair has

Connect from Claude Desktop

Add to ~/.config/claude/claude_desktop_config.json:

json
{  "mcpServers": {    "memex": {      "type": "streamable-http",      "url": "http://localhost:8001/mcp/"    }  }}

Connect from Claude Code

bash
claude mcp add --transport http memex http://localhost:8001/mcp/

Design: why a separate service

The MCP Streamable HTTP transport is session-stateful — initialize, tools/list, and tools/call must all reach the same server process. The REST API runs 4 uvicorn workers with round-robin routing; routing different MCP requests to different workers breaks session state.

Running a dedicated single-worker MCP service on port 8001 avoids sticky-session infrastructure (nginx ip_hash, Redis session store) while keeping the REST API fully multi-worker.


Memory summarisation

Runs as a background asyncio task on a configurable interval (default: every 5 minutes). Finds any (agent_id, user_id) pair where episodic memory count exceeds a threshold, condenses the oldest batch into a single semantic memory, then deletes the originals. Fully local — no LLM API calls.

How it summarises: Pure Python extractive algorithm. Sentences are deduplicated by Jaccard similarity (≥ 0.7 threshold), scored by word frequency (TF), and the top-N are returned in original order. ~1 ms per summarisation, zero dependencies beyond the standard library.

Why episodic-only: Episodic memories are conversation events with natural time-based obsolescence. Semantic and procedural memories encode facts and skills — silently condensing them risks precision loss; they age out via recency decay instead.

Concurrency safety: Uses pg_try_advisory_xact_lock keyed on hashtext(agent_id|user_id). The lock is held only during the DB write transaction, not during the embedding call.

Tune via env vars:

VarDefaultDescription
SUMMARIZATION_ENABLEDtrueToggle the background job
SUMMARIZATION_THRESHOLD100Episodic count to trigger per pair
SUMMARIZATION_BATCH_SIZE50Oldest N memories to condense per run
SUMMARIZATION_INTERVAL_SECONDS300How often the job wakes up

What's next

  • Memory summarisation — background job to condense old episodic memories (local extractive algorithm, zero API calls) when count exceeds threshold
  • Prometheus + Grafana — p50/p99 latency dashboards, embedding API call duration, pool saturation
  • MCP-compatible endpoint — Streamable HTTP server on port 8001; 4 tools (store, search, delete, count); connects to Claude Desktop and Claude Code
  • HNSW index option — flag to switch from ivfflat to HNSW for deployments with >1M vectors
  • Importance-weighted retrieval — factor importance score into ranking formula alongside similarity and recency

Tech stack

LayerChoiceWhy
APIFastAPI + uvicornAsync-first, fast, excellent OpenAPI generation
Embeddingsfastembed ONNX (BAAI/bge-small-en-v1.5)Local, zero API calls, ~12 ms CPU inference, 384-dim
DatabasePostgreSQL 16 + pgvectorRelational + vector in one system, no extra infra
Vector indexivfflatLower build cost than HNSW at this scale
PoolasyncpgDirect control, zero ORM overhead
SummariserPure Python extractiveJaccard dedup + TF scoring, zero ML deps, ~1 ms
RetrytenacityJitter-based backoff on transient errors
MetricsPrometheus + prometheus-fastapi-instrumentatorStandard observability
Load testingLocustPython-native, realistic user simulation

來源:README.md,提交 3e80a83

工具

0
SourceWeft 尚未在沙箱中啟動過該軟體包,因此其工具未知。

版本歷史

1
  1. v0.1.0最新Sep 16, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分