DESIGN.md@docs · git:20260301.d33abf7 · 2026-03-01 · sha256 c54a5994f3251157

DESIGN.md@docs git:20260301.d33abf7A

Immutable. This exact content is served forever at /api/v1/blob/c54a5994f3251157.

# Mnemo — Multi-Agent Shared Memory Service

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

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

What we explicitly DON'T want:
- Complex permission/role systems
- Client-side conflict resolution
- Agents making scope/routing decisions at call time

## 2. Core Model

### Space

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

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-openclaw",              // auto-filled from token
  version: 3                           // auto-managed, for conflict detection
}
```

## 3. Project Structure

Three deliverables:

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

The two client packages are thin wrappers over the API. Core logic lives in the server.

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

### Why Go

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

### Architecture

```
        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                          │
   └──────────────────────────────────────────────────────────────┘
```

## 4. Database Schema

```sql
CREATE TABLE 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.

## 5. API

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"] }
```

`source` is auto-filled from agent_name (derived from token).
If `key` is provided and already exists in the space → upsert (update existing).

#### GET /api/memories — Search / List

```
?q=keyword           Content search
&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)
- `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.

#### 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" }
```

Requires a valid token for this space in the Authorization header.

#### GET /api/spaces/:space_id/info

Returns space name, memory count, agent list.

## 6. Agent Integration

### OpenClaw

Install and configure:

```bash
# 1. Install plugin
openclaw plugins install @mnemo/openclaw-plugin
```

```json
// 2. openclaw.json
{
  "plugins": {
    "slots": { "memory": "mnemo" },
    "entries": {
      "mnemo": {
        "enabled": true,
        "config": {
          "apiUrl": "https://your-server.example.com",
          "apiToken": "mnemo_xxx"
        }
      }
    }
  }
}
```

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"
```

#### 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
```

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 — HTTP

```bash
curl -X POST https://your-server.example.com/api/memories \
  -H "Authorization: Bearer mnemo_xxx" \
  -d '{"content": "...", "key": "topic", "tags": ["tag"]}'
```

## 7. Conflict Resolution

### MVP: Last Writer Wins (lww)

The `version` field is tracked on every write. Conflicts result in overwrite.
Simple, predictable, sufficient for early usage.

### Later: LLM Merge

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}
```

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.

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

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

## 9. Implementation Plan

### Phase 1: Core

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)

### 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)

### Phase 3: Polish

1. Web dashboard for space management
2. Bulk import/export
3. Usage stats