git:20260301.d33abf7 to git:20260302.f28753e

558 added, 275 removed. Audit A to A.

- # Mnemo — Multi-Agent Shared Memory Service
+ # Mnemos — AI Agent Memory, Everywhere
## 1. Problem
- AI agents (Claude Code, OpenClaw, etc.) each maintain their own local memory files.
- These memories are siloed — they can't be shared across agents, machines, or people.
+ AI agents each maintain their own local memory files — siloed, local, forgotten between sessions.
What we want:
- - Multiple agents share a pool of long-term memories via a simple API
- - An agent configures one token + URL, and it just works
- - When two agents update the same memory, the server resolves it automatically
+ - **Individual user**: My agent remembers across sessions, stored in the cloud, zero ops
+ - **Team**: Multiple agents share a pool of memories through a single API
+ - Both work with the same plugin — just different config
What we explicitly DON'T want:
- - Complex permission/role systems
- - Client-side conflict resolution
- - Agents making scope/routing decisions at call time
+ - Forcing users to deploy a server before they can start
+ - Two separate products for "personal" and "team" use cases
+ - Agents dealing with infrastructure details (connection strings, schemas)
- ## 2. Core Model
+ ## 2. Two Modes, One Plugin
- ### Space
+ The core insight: **personal memory and team memory are the same problem at different scales.**
+ ```
+ ┌─────────────────────────────────────────────────────────────────────┐
+ │ Agent Plugin (single codebase) │
+ │ OpenClaw / Claude Code / Any HTTP Client │
+ └──────────────────────────┬──────────────────────────────────────────┘
+ │
+ ┌────────────┴────────────┐
+ │ │
+ has `host` → has `apiUrl` →
+ (direct) (server)
+ │ │
+ ▼ ▼
+ ┌───────────────────┐ ┌───────────────────┐
+ │ TiDB Serverless │ │ mnemo-server │
+ │ │ │ (Go, self-host) │
+ │ Plugin → DB │ │ Plugin → API │
+ │ via HTTP Data API│ │ → DB │
+ │ │ │ │
+ │ Zero deployment │ │ Multi-agent │
+ │ Personal / small │ │ Space management │
+ │ team use │ │ LLM conflict merge│
+ └───────────────────┘ └────────┬───────────┘
+ │
+ ┌───────┴───────┐
+ │ TiDB / MySQL │
+ └───────────────┘
+ ```
+
+ | | Direct Mode | Server Mode |
+ |---|---|---|
+ | **Who** | Individual developer, small team | Organization, multi-agent teams |
+ | **Deploy** | Nothing. TiDB Cloud Serverless free tier | Self-host `mnemo-server` (Go binary or Docker) |
+ | **Config** | Database credentials (`host`/`username`/`password`) | `apiUrl` + `apiToken` |
+ | **Isolation** | Database-level (each DB is a boundary) | Space-level (server manages space_id scoping) |
+ | **Multi-agent sharing** | Share DB credentials = shared memory | Create space, issue tokens per agent |
+ | **Vector search** | Yes (TiDB native VECTOR type) | Yes (server-side embedding + vector) |
+ | **Conflict resolution** | LWW (client-side, simple) | LWW → LLM merge (server-side, Phase 2) |
+ | **Rate limiting** | TiDB Cloud built-in | Server-side per-IP rate limiter |
+
+ **Direct mode is the default.** Mode is inferred from config: `host` present → direct, `apiUrl` present → server.
+ No explicit `mode` field needed. Most users start with direct. If they outgrow it — need space isolation, LLM merge, centralized audit — they switch one config block and everything keeps working.
+
+ ## 3. Core Model
+
+ ### Memory
+
+ A memory is a piece of knowledge with optional structure:
+
+ ```
+ {
+ content: "TiKV compaction: set level0-file-num to 4 for write-heavy...",
+ key: "tikv/compaction-tuning", // optional, for upsert lookup
+ tags: ["tikv", "performance"], // optional, for filtering
+ source: "sj-claude-code", // who wrote it
+ metadata: { severity: "high" }, // optional, arbitrary structured data
+ embedding: [0.012, -0.034, ...], // auto-generated if embedding provider configured
+ version: 3, // auto-managed, for conflict detection
+ score: 0.87 // only in hybrid search responses, omitted otherwise
+ }
+ ```
+
+ ### Space (Server Mode only)
+
A **space** is a shared memory pool. All agents in a space can read/write all memories.
- That's the only sharing concept. No orgs, teams, roles, or hierarchies.
```
Space "backend-team"
├── sj-claude-code (token: mnemo_aaa)
├── sj-openclaw (token: mnemo_bbb)
└── bob-claude (token: mnemo_ccc)
└── Memories: [shared, everyone reads/writes]
```
Want isolation? Different spaces. Want sharing? Same space.
- ### Memory
+ In Direct mode, the **database itself is the space** — no explicit space management needed.
- A memory is a piece of knowledge with optional structure:
+ ## 4. Quick Start
+ ### 30-Second Setup (Direct Mode)
+
+ Create a free TiDB Cloud Serverless cluster at [tidbcloud.com](https://tidbcloud.com), then:
+
+ **Claude Code:**
+ ```bash
+ export MNEMO_DB_HOST="gateway01.us-east-1.prod.aws.tidbcloud.com"
+ export MNEMO_DB_USER="xxx.root"
+ export MNEMO_DB_PASS="xxx"
+ export MNEMO_DB_NAME="mnemos"
+ # Optional: enable vector search
+ export MNEMO_EMBED_API_KEY="sk-..."
```
+
+ Done. Next time you start Claude Code, it auto-creates the table, loads past memories,
+ and saves new ones — all transparently.
+
+ **OpenClaw:**
+ ```json
{
- content: "TiKV compaction: set level0-file-num to 4 for write-heavy...",
- key: "tikv/compaction-tuning", // optional, for upsert lookup
- tags: ["tikv", "performance"], // optional, for filtering
- source: "sj-openclaw", // auto-filled from token
- version: 3 // auto-managed, for conflict detection
+ "plugins": {
+ "slots": { "memory": "mnemo" },
+ "entries": {
+ "mnemo": {
+ "enabled": true,
+ "config": {
+ "host": "gateway01.us-east-1.prod.aws.tidbcloud.com",
+ "username": "xxx.root",
+ "password": "xxx",
+ "database": "mnemos"
+ }
+ }
+ }
+ }
}
```
- ## 3. Project Structure
+ ### Team Setup (Server Mode)
- Three deliverables:
+ ```bash
+ # 1. Deploy server
+ cd server && MNEMO_DSN="user:pass@tcp(host:4000)/mnemos" go run ./cmd/mnemo-server
- | Component | What | Form |
- |-----------|------|------|
- | **mnemo-server** | API service + database | Go binary, deployed as container or single binary |
- | **@mnemo/openclaw-plugin** | OpenClaw agent integration | npm package, `kind: "memory"` plugin |
- | **mnemo-ccplugin** | Claude Code agent integration | Claude Code Plugin (Hooks + Skills) |
+ # 2. Create space
+ curl -X POST localhost:8080/api/spaces \
+ -d '{"name":"backend-team","agent_name":"alice-claude","agent_type":"claude_code"}'
+ # → {"ok":true, "space_id":"...", "api_token":"mnemo_abc"}
- The two client packages are thin wrappers over the API. Core logic lives in the server.
+ # 3. Configure agents (apiUrl present → server mode)
+ export MNEMO_API_URL="http://localhost:8080"
+ export MNEMO_API_TOKEN="mnemo_abc"
+ ```
+ ## 5. Direct Mode: How It Works
+
+ ### The Key Idea: TiDB Serverless HTTP Data API
+
+ TiDB Cloud Serverless exposes an HTTP endpoint for SQL:
+
+ ```bash
+ curl -X POST "https://http-${MNEMO_DB_HOST}/v1beta/sql" \
+ -u "${MNEMO_DB_USER}:${MNEMO_DB_PASS}" \
+ -H "Content-Type: application/json" \
+ -d '{"database":"mnemos","query":"SELECT * FROM memories ORDER BY updated_at DESC LIMIT 20"}'
```
- mnemos/
- ├── server/ # Go API server
- │ ├── cmd/mnemo-server/
- │ │ └── main.go # Entry point, DI wiring, graceful shutdown
- │ ├── internal/
- │ │ ├── config/config.go # Environment variable loading
- │ │ ├── domain/
- │ │ │ ├── types.go # Core types (Memory, SpaceToken, AuthInfo, etc.)
- │ │ │ ├── errors.go # Sentinel errors (ErrNotFound, ErrConflict, etc.)
- │ │ │ └── tokengen.go # Token generation (mnemo_ + 32 hex)
- │ │ ├── handler/
- │ │ │ ├── handler.go # Router setup, JSON helpers, error mapping
- │ │ │ ├── memory.go # CRUD + search + upsert + bulk
- │ │ │ └── space.go # Space creation + token management
- │ │ ├── middleware/
- │ │ │ ├── auth.go # Token → space_id + agent_name via context
- │ │ │ └── ratelimit.go # Per-IP token bucket rate limiter
- │ │ ├── repository/
- │ │ │ ├── repository.go # MemoryRepo + SpaceTokenRepo interfaces
- │ │ │ └── tidb/
- │ │ │ ├── tidb.go # *sql.DB setup (pool config, ping)
- │ │ │ ├── memory.go # MemoryRepo SQL implementation
- │ │ │ └── space_token.go # SpaceTokenRepo SQL implementation
- │ │ └── service/
- │ │ ├── memory.go # Business logic (upsert, LWW, validation, bulk)
- │ │ └── space.go # Space creation, token generation, space info
- │ ├── schema.sql # Database DDL
- │ ├── Dockerfile # Multi-stage build
- │ ├── go.mod
- │ └── go.sum
- │
- ├── openclaw-plugin/ # OpenClaw plugin (kind: "memory")
- │ ├── index.ts # Register memory_store/search/get/update/delete
- │ ├── api-client.ts # HTTP client for mnemo server
- │ ├── openclaw.plugin.json
- │ └── package.json
- │
- ├── ccplugin/ # Claude Code Plugin (Hooks + Skills)
- │ ├── .claude-plugin/
- │ │ └── plugin.json # Plugin manifest
- │ ├── hooks/
- │ │ ├── hooks.json # Hook definitions (4 lifecycle hooks)
- │ │ ├── common.sh # Shared: env, API client helpers (curl → mnemo API)
- │ │ ├── session-start.sh # Load recent memories → additionalContext
- │ │ ├── user-prompt-submit.sh # Hint: "[mnemo] Memory available"
- │ │ ├── stop.sh # Summarize last turn → POST /api/memories
- │ │ └── session-end.sh # Cleanup
- │ └── skills/
- │ └── memory-recall/
- │ └── SKILL.md # Semantic recall skill (context: fork)
- │
- ├── assets/logo.png # Project logo
- ├── docs/DESIGN.md # Full design document
- ├── README.md
- ├── CLAUDE.md # Agent-readable project context
- ├── CONTRIBUTING.md
- ├── Makefile
- ├── LICENSE # Apache-2.0
- └── .gitignore
+
+ This means the Claude Code hooks can **stay pure bash + curl** in Direct mode too.
+ No `mysql` CLI, no Go binary, no Python package — the same zero-dependency story as Server mode.
+
+ For the OpenClaw plugin, `@tidbcloud/serverless` provides a native JS driver over HTTP.
+
+ ### Auto Schema Init
+
+ On first connection, the plugin checks if the `memories` table exists and creates it if not:
+
+ ```sql
+ CREATE TABLE IF NOT EXISTS memories (
+ id VARCHAR(36) PRIMARY KEY,
+ space_id VARCHAR(36) NOT NULL, -- in direct mode: a fixed value derived from DB name
+ content TEXT NOT NULL,
+ key_name VARCHAR(255),
+ source VARCHAR(100),
+ tags JSON,
+ metadata JSON,
+ embedding VECTOR(${dims}) NULL, -- dims from config (default 1536), nullable
+ version INT DEFAULT 1,
+ updated_by VARCHAR(100),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE INDEX idx_key (space_id, key_name),
+ INDEX idx_space (space_id),
+ INDEX idx_source (space_id, source),
+ INDEX idx_updated (space_id, updated_at)
+ );
```
- ### Why Go
+ The `${dims}` value comes from `MNEMO_EMBED_DIMS` (default 1536). Must match the
+ embedding model's output dimensions (e.g., `text-embedding-3-small` = 1536,
+ `nomic-embed-text` = 768).
- - **Single binary deployment** — no runtime, no node_modules. Build once, run anywhere (container or bare metal).
- - **Goroutines** — natural fit for IO-bound workload (DB queries, LLM API calls in Phase 2).
- - **go-sql-driver/mysql** — mature, battle-tested MySQL driver, works directly with TiDB.
- - **Long-term extensibility** — when adding vector search or LLM merge, Go can call any REST API; no Python SDK dependency needed.
+ The VECTOR column is nullable — works on all TiDB Serverless clusters. The vector index
+ is added in a **separate** `ALTER TABLE` that silently fails (try/catch, no error propagation)
+ when the index already exists or TiFlash is unavailable — keyword-only search as fallback:
- ### Architecture
+ ```sql
+ ALTER TABLE memories ADD VECTOR INDEX idx_cosine ((VEC_COSINE_DISTANCE(embedding)));
+ -- silent failure ok: index exists or TiFlash unavailable
+ ```
+ ### Direct Mode Isolation
+
+ In Direct mode, `space_id` is set to a fixed value `"default"`. All queries still include
+ `WHERE space_id = ?` for schema compatibility with Server mode. This means:
+
+ - Same table structure across both modes
+ - Migrating from Direct → Server is a data export/import (update space_id values)
+ - Multiple users sharing the same DB credentials = shared memory (the simple version of spaces)
+
+ ## 6. Architecture
+
+ ### Direct Mode
+
```
- Claude Code OpenClaw Any Agent
- ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐
- │ mnemo-ccplugin │ │ @mnemo/ │ │ HTTP Client │
- │ (Hooks + Skills) │ │ openclaw-plugin │ │ │
- │ │ │ (kind: "memory") │ │ │
- │ SessionStart: │ │ │ │ │
- │ load memories │ │ │ │ │
- │ Stop: │ │ │ │ │
- │ save memories │ │ │ │ │
- │ Skill: │ │ │ │ │
- │ recall memories │ │ │ │ │
- └───────┬──────────┘ └────────┬─────────┘ └──────┬───────┘
- │ │ │
- ▼ ▼ ▼
- ┌──────────────────────────────────────────────────────────────┐
- │ mnemo-server (Go) │
- │ │
- │ Auth: Bearer token → space_id + agent_name │
- │ Conflict: server-side auto-resolve (lww → llm merge) │
- │ Search: keyword (MVP), vector + keyword (later) │
- └──────────────────────────┬───────────────────────────────────┘
- │
- ▼
- ┌──────────────────────────────────────────────────────────────┐
- │ TiDB Cloud │
- │ Row-level isolation via space_id │
- └──────────────────────────────────────────────────────────────┘
+ Claude Code OpenClaw Any Client
+ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐
+ │ ccplugin │ │ openclaw- │ │ curl / fetch │
+ │ (Hooks+Skills) │ │ plugin │ │ │
+ │ │ │ │ │ │
+ │ bash + curl │ │ @tidbcloud/ │ │ HTTP POST │
+ │ → HTTP Data API│ │ serverless │ │ → SQL endpoint │
+ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘
+ │ │ │
+ └──────────┬──────────┴──────────────────────┘
+ ▼
+ ┌─────────────────────┐
+ │ TiDB Serverless │
+ │ HTTP Data API │
+ │ │
+ │ POST /v1beta/sql │
+ │ Basic Auth │
+ │ VECTOR + keyword │
+ └─────────────────────┘
```
- ## 4. Database Schema
+ ### Server Mode
+ ```
+ Claude Code OpenClaw Any Client
+ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐
+ │ ccplugin │ │ openclaw- │ │ curl / fetch │
+ │ (Hooks+Skills) │ │ plugin │ │ │
+ │ │ │ │ │ │
+ │ bash + curl │ │ HTTP client │ │ HTTP client │
+ │ → mnemo API │ │ → mnemo API │ │ → mnemo API │
+ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘
+ │ │ │
+ └──────────┬──────────┴──────────────────────┘
+ ▼
+ ┌─────────────────────┐
+ │ mnemo-server (Go) │
+ │ │
+ │ Bearer token auth │
+ │ Space management │
+ │ Upsert + versioning│
+ │ Hybrid search │
+ │ Rate limiting │
+ │ LLM merge (Phase 2)│
+ └──────────┬──────────┘
+ │
+ ▼
+ ┌─────────────────────┐
+ │ TiDB / MySQL │
+ └─────────────────────┘
+ ```
+
+ ## 7. Plugin Design: Backend Abstraction
+
+ Both plugins use a **backend abstraction** — the 5 memory tools (store/search/get/update/delete)
+ call through an interface. The config fields determine which backend (`host` → direct, `apiUrl` → server):
+
+ - **Direct backend**: `@tidbcloud/serverless` (OpenClaw) or `curl → TiDB HTTP Data API` (Claude Code) → SQL
+ - **Server backend**: `fetch` (OpenClaw) or `curl` (Claude Code) → mnemo-server REST API
+
+ The tool registration code and hook scripts are mode-agnostic — they call the same
+ helper functions regardless of which backend is active.
+
+ ## 8. Search: Keyword + Vector (Hybrid)
+
+ ### Design Principle: Graceful Degradation
+
+ ```
+ Embedding provider configured?
+ ┌─────────┴─────────┐
+ Yes No
+ │ │
+ Hybrid search Keyword only
+ (vector + keyword) (LIKE '%q%')
+ │
+ ┌─────────┴─────────┐
+ Vector results Keyword results
+ (ANN cosine) (substring match)
+ │ │
+ └─────────┬──────────┘
+ Merge & rank
+ (vector score priority,
+ keyword-only gets 0.5)
+ ```
+
+ Vector search is **opt-in but zero-effort to enable**:
+ - No embedding config → keyword search works immediately
+ - Add an OpenAI key (or Ollama URL) → hybrid search activates automatically
+ - No schema migration needed — VECTOR column is nullable from day one
+
+ ### Embedder Abstraction
+
+ The embedding provider is wrapped behind a simple interface (`embed(text) → float[]` + `dims`).
+ A factory returns `null` when unconfigured — every CRUD function accepts the embedder as
+ nullable, skipping vector operations when absent. No error, no special handling.
+
+ Internally uses the OpenAI SDK with `baseURL` override for Ollama/LM Studio/custom endpoints.
+
+ ### Embedding Provider Configuration
+
+ All fields are optional. Omitting everything → keyword-only mode.
+
+ ```bash
+ # OpenAI (default: text-embedding-3-small, 1536 dims)
+ export MNEMO_EMBED_API_KEY="sk-..."
+
+ # Ollama (local, free, e.g. nomic-embed-text = 768 dims)
+ export MNEMO_EMBED_BASE_URL="http://localhost:11434/v1"
+ export MNEMO_EMBED_MODEL="nomic-embed-text"
+ export MNEMO_EMBED_DIMS="768"
+
+ # Any OpenAI-compatible endpoint
+ export MNEMO_EMBED_BASE_URL="https://your-embeddings.example.com/v1"
+ export MNEMO_EMBED_API_KEY="..."
+ export MNEMO_EMBED_MODEL="text-embedding-3-small"
+ export MNEMO_EMBED_DIMS="1536"
+ ```
+
+ | Field | Default | Notes |
+ |-------|---------|-------|
+ | `MNEMO_EMBED_API_KEY` | — | OpenAI key. For local providers (Ollama), omit or set to `"local"` |
+ | `MNEMO_EMBED_BASE_URL` | OpenAI default | Override for Ollama (`http://localhost:11434/v1`), LM Studio, etc. |
+ | `MNEMO_EMBED_MODEL` | `text-embedding-3-small` | Model name passed to embeddings API |
+ | `MNEMO_EMBED_DIMS` | `1536` | Vector dimensions. **Must match model output**. Used in `VECTOR(dims)` DDL |
+
+ **Critical implementation detail**: When calling the embedding API, always set
+ `encoding_format: "float"`. Ollama and LM Studio default to base64 encoding which
+ is incompatible with TiDB's VECTOR type. The `"float"` format is also accepted by
+ OpenAI, so this is safe to always set.
+
+ ### Where Embeddings Are Generated
+
+ | Mode | Where |
+ |------|-------|
+ | **Direct** | Plugin-side. OpenClaw plugin calls OpenAI/Ollama before INSERT. Claude Code hooks call the embedding API and include the vector in the SQL. |
+ | **Server** | Server-side. The Go server calls the embedding API on write and on search. Agents don't deal with embeddings at all. |
+
+ ### When Embeddings Are Generated
+
+ | Operation | Embedding behavior |
+ |-----------|-------------------|
+ | **Store** | If embedder exists, embed `content` → store in `embedding` column. If no embedder, `embedding = NULL`. |
+ | **Update** | Re-generate embedding **only if `content` changed** AND embedder exists. If only tags/metadata change, embedding stays as-is. |
+ | **Search** | If embedder exists and `q` is provided, embed the query → hybrid search. Otherwise keyword-only. |
+ | **Single failure** | If embedding fails on a single record (API timeout, etc.), the error propagates — the write/search fails. This is intentional: partial embedding corruption is worse than a retry. |
+
+ ### Hybrid Search Algorithm
+
+ When `q` is provided and an embedder is available:
+
+ 1. **Embed the query**: `queryVec = embedder.embed(q)`
+
+ 2. **Vector search** (ANN): Fetch `limit × 3` results for merge headroom.
+ ```sql
+ SELECT *, VEC_COSINE_DISTANCE(embedding, ?) AS distance
+ FROM memories
+ WHERE space_id = ? AND embedding IS NOT NULL [AND other filters]
+ ORDER BY VEC_COSINE_DISTANCE(embedding, ?)
+ LIMIT ?
+ ```
+ **Critical**: `VEC_COSINE_DISTANCE` must appear identically in both SELECT and ORDER BY —
+ this is required for TiDB to use the VECTOR INDEX (ANN scan). Different expressions
+ cause a full table scan.
+
+ The `embedding IS NOT NULL` filter is mandatory — ANN queries on NULL vectors fail.
+
+ 3. **Keyword search**: Also fetch `limit × 3` results.
+ ```sql
+ SELECT * FROM memories
+ WHERE space_id = ? AND content LIKE CONCAT('%', ?, '%') [AND other filters]
+ ORDER BY updated_at DESC
+ LIMIT ?
+ ```
+
+ 4. **Merge & de-duplicate** (by memory ID):
+ - Vector results: `score = 1 - distance` (cosine distance → similarity, range 0–1)
+ - Keyword-only results (not in vector set): `score = 0.5` (neutral)
+ - If a memory appears in both sets, the vector score wins (higher precision)
+
+ 5. **Sort & paginate**: Sort merged results by score descending, then `slice(offset, offset + limit)`.
+ Pagination happens **after** merge, not before — this ensures correct ordering across both result sets.
+
+ 6. **Response**: Each memory includes an optional `score` field (only present in hybrid search results,
+ omitted in keyword-only or non-search responses).
+
+ When no embedder is available, steps 1–2 are skipped — pure keyword search, no score field.
+
+ ## 9. Database Schema
+
+ ### Unified Schema (both modes)
+
```sql
- CREATE TABLE space_tokens (
+ CREATE TABLE IF NOT EXISTS memories (
+ id VARCHAR(36) PRIMARY KEY,
+ space_id VARCHAR(36) NOT NULL,
+ content TEXT NOT NULL,
+ key_name VARCHAR(255),
+ source VARCHAR(100),
+ tags JSON,
+ metadata JSON,
+ embedding VECTOR(${dims}) NULL, -- dims from MNEMO_EMBED_DIMS (default 1536)
+ version INT DEFAULT 1,
+ updated_by VARCHAR(100),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE INDEX idx_key (space_id, key_name),
+ INDEX idx_space (space_id),
+ INDEX idx_source (space_id, source),
+ INDEX idx_updated (space_id, updated_at)
+ );
+ ```
+
+ ### Server Mode additional table
+
+ ```sql
+ CREATE TABLE IF NOT EXISTS space_tokens (
api_token VARCHAR(64) PRIMARY KEY,
space_id VARCHAR(36) NOT NULL,
space_name VARCHAR(255) NOT NULL,
agent_name VARCHAR(100) NOT NULL,
agent_type VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_space (space_id)
);
-
- CREATE TABLE memories (
- id VARCHAR(36) PRIMARY KEY,
- space_id VARCHAR(36) NOT NULL,
- content TEXT NOT NULL,
- key_name VARCHAR(255),
- source VARCHAR(100),
- tags JSON,
- version INT DEFAULT 1,
- updated_by VARCHAR(100),
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- INDEX idx_space (space_id),
- INDEX idx_key (space_id, key_name),
- INDEX idx_source (space_id, source),
- INDEX idx_updated (space_id, updated_at)
- );
```
- Two tables. `space_tokens` maps tokens to spaces and identifies agents.
- A space exists implicitly — no separate spaces table needed.
+ ### Schema Differences
- ## 5. API
+ | Column | Direct Mode | Server Mode |
+ |--------|------------|-------------|
+ | `space_id` | Fixed value (derived from DB name) | Server-managed, maps to space |
+ | `embedding` | Plugin generates (if configured) | Server generates (if configured) |
+ | `metadata` | Full JSON support | Full JSON support |
+ | `version` | Auto-incremented on write | Atomic `version = version + 1` in SQL |
+ The `memories` table is **identical** across modes. This makes Direct → Server migration
+ a simple data export/import.
+
+ ## 10. API (Server Mode)
+
Auth: `Authorization: Bearer <api_token>`
Server resolves token → space_id + agent_name. All queries auto-scoped to space.
### Memory CRUD
#### POST /api/memories — Create
```json
- { "content": "...", "key": "optional/key", "tags": ["optional"] }
+ { "content": "...", "key": "optional/key", "tags": ["optional"], "metadata": {} }
```
`source` is auto-filled from agent_name (derived from token).
If `key` is provided and already exists in the space → upsert (update existing).
+ If embedding is configured, server generates embedding before write.
#### GET /api/memories — Search / List
```
- ?q=keyword Content search
+ ?q=keyword Hybrid search (vector + keyword if embedder configured)
&tags=tag1,tag2 Filter by tags (AND)
&source=sj-openclaw Filter by author
&key=tikv/tuning Filter by key
&limit=50&offset=0
```
#### GET /api/memories/:id
#### PUT /api/memories/:id — Update
```
Header: If-Match: 3 (optional)
Body: { "content": "updated", "tags": [...] }
```
- - No `If-Match` → direct overwrite (lww)
+ - No `If-Match` → direct overwrite (LWW)
- `If-Match` matches current version → write, version++
- - `If-Match` mismatch → server auto-resolves (MVP: lww, later: llm merge)
-
- Response always includes `version` for client to track.
+ - `If-Match` mismatch → server auto-resolves (MVP: LWW, later: LLM merge)
#### DELETE /api/memories/:id
#### POST /api/memories/bulk
```json
{ "memories": [{ "content": "...", "key": "...", "tags": [...] }, ...] }
```
### Space Management
#### POST /api/spaces — Create space + first agent token
```json
{
"name": "backend-team",
"agent_name": "sj-openclaw",
"agent_type": "openclaw"
}
→ { "ok": true, "space_id": "uuid", "api_token": "mnemo_xxx" }
```
#### POST /api/spaces/:space_id/tokens — Add agent to space
- ```json
- {
- "agent_name": "sj-claude-code",
- "agent_type": "claude_code"
- }
- → { "ok": true, "api_token": "mnemo_yyy" }
+ #### GET /api/spaces/:space_id/info — Space metadata
+
+ ## 11. Agent Integration
+
+ ### Claude Code Plugin
+
+ Uses Claude Code's native Hooks + Skills. Memory capture and recall are fully automatic.
+
+ ```bash
+ # Direct mode (DB credentials present → direct)
+ export MNEMO_DB_HOST="gateway01.us-east-1.prod.aws.tidbcloud.com"
+ export MNEMO_DB_USER="xxx.root"
+ export MNEMO_DB_PASS="xxx"
+ export MNEMO_DB_NAME="mnemos"
+
+ # Or server mode (apiUrl present → server)
+ export MNEMO_API_URL="http://localhost:8080"
+ export MNEMO_API_TOKEN="mnemo_xxx"
```
- Requires a valid token for this space in the Authorization header.
+ | Hook | Async | What it does |
+ |------|-------|-------------|
+ | **SessionStart** | no | Load 20 most recent memories → inject as `additionalContext` |
+ | **UserPromptSubmit** | no | Return hint: `"[mnemo] Shared memory available"` |
+ | **Stop** | yes | Summarize last turn (via haiku), save as new memory |
+ | **SessionEnd** | no | Cleanup |
- #### GET /api/spaces/:space_id/info
+ Plus **memory-recall** skill (`context: fork`) for on-demand search.
- Returns space name, memory count, agent list.
+ ### OpenClaw Plugin
- ## 6. Agent Integration
+ Declares `kind: "memory"`, replacing the built-in memory provider.
- ### OpenClaw
+ **Why plugin (kind: "memory") instead of skill?**
- Install and configure:
+ | | Plugin (`kind: "memory"`) | Skill |
+ |---|---|---|
+ | Trigger | Framework calls automatically | Agent decides when to call |
+ | Lifecycle | Framework manages load/save timing | Agent must remember to read/write |
+ | Integration | Replaces built-in `memory_*` tools | Adds extra tools alongside built-in |
+ | Reliability | Guaranteed execution | Depends on agent judgment |
- ```bash
- # 1. Install plugin
- openclaw plugins install @mnemo/openclaw-plugin
- ```
+ Memory should be **automatic, not optional**. A `kind: "memory"` plugin replaces OpenClaw's
+ built-in memory slot — the framework guarantees memory is always read and written at the
+ right lifecycle points. A skill would require the agent to judge when to store and recall,
+ making memory unreliable.
+ This is the same philosophy as the Claude Code side: Hooks (automatic) over MCP tools (manual).
+
```json
- // 2. openclaw.json
{
- "plugins": {
- "slots": { "memory": "mnemo" },
- "entries": {
- "mnemo": {
- "enabled": true,
- "config": {
- "apiUrl": "https://your-server.example.com",
- "apiToken": "mnemo_xxx"
- }
+ "mnemo": {
+ "enabled": true,
+ "config": {
+ "host": "gateway01.us-east-1.prod.aws.tidbcloud.com",
+ "username": "xxx.root",
+ "password": "xxx",
+ "database": "mnemos",
+ "embedding": {
+ "apiKey": "sk-...",
+ "model": "text-embedding-3-small"
}
}
}
}
```
- That's it. The plugin declares `kind: "memory"`, replacing the built-in
- memory-core. All memory operations go to the remote mnemo server.
-
- Tools exposed to agent:
- ```
- memory_store(content, key?, tags?) → POST /api/memories
- memory_search(q?, tags?, source?) → GET /api/memories
- memory_get(id) → GET /api/memories/:id
- memory_update(id, content?, tags?) → PUT /api/memories/:id
- memory_delete(id) → DELETE /api/memories/:id
- ```
-
- ### Claude Code — Plugin (Hooks + Skills)
-
- Inspired by [memsearch](https://github.com/zilliztech/memsearch)'s Claude Code Plugin.
- Uses Claude Code's native Hooks and Skills system — no MCP server needed.
- Memory capture and recall are fully automatic.
-
- ```bash
- # Install
- /plugin marketplace add mashenjun/mnemo # or local: claude --plugin-dir ./ccplugin
- ```
-
- Configure via environment variables:
- ```bash
- export MNEMO_API_URL="https://your-server.example.com"
- export MNEMO_API_TOKEN="mnemo_xxx"
+ Tools exposed (same in both modes):
```
-
- #### How It Works
-
- The plugin hooks into 4 Claude Code lifecycle events:
-
- | Hook | Async | What it does |
- |------|-------|-------------|
- | **SessionStart** | no | `GET /api/memories?limit=20` → inject recent memories as `additionalContext` |
- | **UserPromptSubmit** | no | Return `systemMessage: "[mnemo] Memory available"` as hint to Claude |
- | **Stop** | yes | Summarize last turn (via `claude -p --model haiku`), then `POST /api/memories` to save |
- | **SessionEnd** | no | Cleanup |
-
- Plus a **memory-recall skill** (`context: fork`):
-
- ```markdown
- ---
- name: memory-recall
- description: "Search shared memories from past sessions. Use when the user's
- question could benefit from historical context, past decisions, or project knowledge."
- context: fork
- allowed-tools: Bash
- ---
-
- You are a memory retrieval agent. Search shared memories and return relevant context.
-
- ## Steps
- 1. Search: curl GET $MNEMO_API_URL/api/memories?q=<query>&limit=10
- 2. Evaluate: skip irrelevant results
- 3. Return a curated summary of relevant memories to the main conversation
+ memory_store(content, key?, tags?, metadata?)
+ memory_search(q?, tags?, source?, key?, limit?, offset?)
+ memory_get(id)
+ memory_update(id, content?, tags?, metadata?)
+ memory_delete(id)
```
- When Claude judges the user's question needs historical context, it auto-invokes
- this skill. The skill runs in a **forked subagent** — intermediate search results
- stay isolated, only the curated summary enters the main context.
-
- #### Why Hooks + Skills instead of MCP
-
- | Aspect | MCP Server | Hooks + Skills |
- |--------|-----------|---------------|
- | Memory capture | Manual — Claude must decide to call `memory_store` | Automatic — Stop hook summarizes and saves every session |
- | Session start context | None — Claude must call `memory_search` first | Automatic — SessionStart injects recent memories |
- | Recall trigger | Claude must decide to call MCP tool | Automatic — Claude sees "[mnemo] Memory available" hint, invokes skill when needed |
- | Context cost | MCP tool definitions permanently in context | Skill runs in fork, zero main context cost |
- | Dependencies | Node.js MCP server process | Shell scripts + curl (zero dependencies) |
+ ### Any Agent — Plain HTTP
- ### Any Agent — HTTP
+ Works in both modes:
```bash
- curl -X POST https://your-server.example.com/api/memories \
+ # Server mode
+ curl -X POST https://your-server/api/memories \
-H "Authorization: Bearer mnemo_xxx" \
-d '{"content": "...", "key": "topic", "tags": ["tag"]}'
+
+ # Direct mode (TiDB HTTP Data API)
+ curl -X POST "https://http-${HOST}/v1beta/sql" \
+ -u "${USER}:${PASS}" \
+ -d '{"database":"mnemos","query":"INSERT INTO memories ..."}'
```
- ## 7. Conflict Resolution
+ ## 12. Conflict Resolution
- ### MVP: Last Writer Wins (lww)
+ ### LWW (Last Writer Wins) — Both Modes
The `version` field is tracked on every write. Conflicts result in overwrite.
- Simple, predictable, sufficient for early usage.
+ Simple, predictable, sufficient for most cases.
- ### Later: LLM Merge
+ ### LLM Merge — Server Mode, Phase 2
When enabled per space, version conflicts trigger an LLM call:
```
Two agents updated the same memory. Merge into one coherent version.
- Preserve all important information from both
- Remove duplicates
- Keep markdown formatting
- Version A (current in DB):
- {current_content}
-
- Version B (incoming):
- {new_content}
+ Version A (current): {current_content}
+ Version B (incoming): {new_content}
```
- Server handles this transparently. Agent's PUT still returns 200.
- The `version` field and `If-Match` support from day one ensure this
- can be added without any API changes.
+ Server handles this transparently. The agent's PUT still returns 200.
- ## 8. Scope Boundaries
+ ## 13. Project Structure
+ ```
+ mnemos/
+ ├── server/ # Go API server (server mode backend)
+ │ ├── cmd/mnemo-server/
+ │ │ └── main.go
+ │ ├── internal/
+ │ │ ├── config/ # Env var loading
+ │ │ ├── domain/ # Core types, errors, token generation
+ │ │ ├── handler/ # HTTP handlers + chi router
+ │ │ ├── middleware/ # Auth + rate limiter
+ │ │ ├── repository/ # Interface + TiDB implementation
+ │ │ └── service/ # Business logic (upsert, LWW, search, embedding)
+ │ ├── schema.sql
+ │ └── Dockerfile
+ │
+ ├── openclaw-plugin/ # OpenClaw agent plugin (TypeScript)
+ │ ├── index.ts # Tool registration (mode-agnostic)
+ │ ├── backend.ts # MemoryBackend interface
+ │ ├── direct-backend.ts # Direct mode: @tidbcloud/serverless → SQL
+ │ ├── server-backend.ts # Server mode: fetch → mnemo API
+ │ ├── embedder.ts # Embedding provider (OpenAI/Ollama/any)
+ │ ├── schema.ts # Auto schema init (direct mode)
+ │ ├── openclaw.plugin.json
+ │ └── package.json
+ │
+ ├── ccplugin/ # Claude Code plugin (Hooks + Skills)
+ │ ├── .claude-plugin/
+ │ │ └── plugin.json
+ │ ├── hooks/
+ │ │ ├── hooks.json
+ │ │ ├── common.sh # Mode-aware helpers (server: curl→API, direct: curl→SQL)
+ │ │ ├── session-start.sh
+ │ │ ├── user-prompt-submit.sh
+ │ │ ├── stop.sh
+ │ │ └── session-end.sh
+ │ └── skills/
+ │ └── memory-recall/
+ │ └── SKILL.md
+ │
+ ├── assets/logo.png
+ ├── docs/DESIGN.md
+ ├── README.md
+ ├── CLAUDE.md
+ ├── CONTRIBUTING.md
+ ├── Makefile
+ ├── LICENSE
+ └── .gitignore
+ ```
+
+ ## 14. Scope Boundaries
+
What this system does:
- - Shared long-term memory across agents via REST API
- - Keyword search (MVP), vector search (later)
- - Server-side conflict resolution
- - Simple token-based auth
+ - Cloud-persistent memory for AI agents (personal or shared)
+ - Keyword + vector hybrid search with graceful degradation
+ - Two connectivity modes: direct-to-database and server-mediated
+ - Automatic memory capture and recall via agent plugins
+ - Server-side conflict resolution (LWW now, LLM merge later)
What this system does NOT do:
- - Local/private memory (each agent handles its own)
- - Real-time sync or collaboration
- - Permission/role management
- - Embedding generation on client side
+ - Local-only memory (each agent handles its own)
+ - Real-time sync or collaborative editing
+ - Permission/role management beyond spaces
+ - Embedding model hosting (uses external APIs)
- ## 9. Implementation Plan
+ ## 15. Implementation Plan
- ### Phase 1: Core
+ ### Phase 1: Core + Direct Mode
- 1. Database schema (2 tables) — TiDB Cloud
- 2. Go API server: space management + memory CRUD + auth + keyword search + upsert
- 3. OpenClaw plugin (kind: "memory", TypeScript, calls API)
- 4. Claude Code plugin (Hooks + Skills, bash + curl, calls API)
+ 1. ~~Go API server: CRUD + auth + keyword search + upsert~~ ✅
+ 2. ~~OpenClaw plugin (server mode)~~ ✅
+ 3. ~~Claude Code plugin (server mode)~~ ✅
+ 4. **Direct mode for OpenClaw plugin**: `DirectBackend` + `@tidbcloud/serverless` + auto schema init
+ 5. **Direct mode for Claude Code plugin**: `common.sh` mode-aware helpers using TiDB HTTP Data API
+ 6. **Schema evolution**: Add `metadata JSON` and `embedding VECTOR(1536)` columns
+ 7. **Hybrid search**: Embedder abstraction + vector search in both modes
### Phase 2: Smart Features
- 1. LLM conflict merge — Go server calls LLM REST API (configurable per space)
- 2. Server-side embedding generation + vector search
- 3. Hybrid search (vector + keyword)
+ 1. Server-side embedding generation (Go server calls OpenAI/Ollama on write)
+ 2. LLM conflict merge (configurable per space)
+ 3. Auto-tagging via LLM on write
### Phase 3: Polish
1. Web dashboard for space management
2. Bulk import/export
- 3. Usage stats
+ 3. Usage analytics
+ 4. `mnemo setup` CLI wizard for one-command onboarding