Persistent, bi-temporal memory for LLM agents — local-first, MCP-native, Obsidian-readable.
pip install seahorse-memory
seahorse init myvault && seahorse remember "Sergio lives in Madrid"
seahorse recall "where does Sergio live?"
LLM agents start every session from zero. The context window is not memory: it is a scratchpad that resets, and it is too small to hold what an agent has learned across weeks of work. The tools that try to fix this have their own problems:
Seahorse is a different approach: an open, portable, bi-temporal memory standard that an agent writes to and reads from, that a human can read and correct, and that does not lock you into any runtime or provider.
The fastest way to see Seahorse is to give Claude Code a memory that survives between sessions. Three steps:
1. Capture sessions. seahorse setup installs the observer hooks into
~/.claude/settings.json; seahorse observe start runs the capture worker.
Every session is recorded as episodes — skip-first (near-zero cost),
redacted, with a deterministic summary.
seahorse setup
seahorse observe start
2. Recall across sessions. The SessionStart hook injects seahorse context
into the next session, so the agent starts with what it learned before. Ask
directly with seahorse recall:
seahorse context
seahorse recall "what did we decide about the API design?"
3. Bring your existing memory. If you already use claude-mem, seahorse import migrates its observations into canonical episodes — no replay, no
lock-in:
seahorse import --mode commit
The key difference: the agent writes into the same vault you edit in Obsidian. Every episode is a markdown file with YAML frontmatter — readable, editable, diffable in git, and auditable by a human. The agent's memory is not a black box; it is your notes.
Seahorse is built for agents: the memory surface is a stdio MCP server
(io.seahorse.memory/v1) that any agent that speaks MCP can connect to. The
CLI is for humans and scripts; agents talk to seahorse-mcp.
Register the server in Claude Code (local scope, default):
claude mcp add seahorse-mcp -- uvx --from seahorse-memory seahorse-mcp --vault "${HOME}/myvault"
The -- is required — it separates Claude's own flags from the server command.
Use --scope project to share the server with a team via .mcp.json (checked
into git). Verify with claude mcp list (should show ✔ Connected) and
claude mcp get seahorse-mcp.
Or configure it in .mcp.json at the project root (works with any MCP
client):
{
"mcpServers": {
"seahorse-mcp": {
"type": "stdio",
"command": "uvx",
"args": ["--from", "seahorse-memory", "seahorse-mcp", "--vault", "${HOME}/myvault"]
}
}
}
Note: ~ is not expanded in .mcp.json — use ${HOME} or an absolute path.
(mcpServers in settings.json is silently ignored; MCP servers live in
~/.claude.json for user/local scope and in .mcp.json for project scope.)
Once connected, the agent sees the 14 memory tools — remember, recall,
recall_timeline, recall_full, improve, forget, build_pit,
skill_add, skill_show, skill_list, skill_search, freshness_view,
audit_log, follow_supersedes_chain (see The agent
surface).
The observer (seahorse setup) is a separate piece: it captures Claude Code
sessions into episodes. The MCP server is how the agent reads and writes
memory. Both work together — capture sessions, then recall across them.
graph LR
A[Claude Code / any MCP agent] -- stdio MCP io.seahorse.memory/v1 --> S[seahorse-mcp]
S --> E[Bi-temporal engine]
E --> DB[(sqlite3 + sqlite-vec + FTS5)]
E --> V[Obsidian vault: markdown + F3.1 frontmatter]
H[Human in Obsidian] --> V
An agent talks to seahorse-mcp over stdio MCP. The engine stores every
episode twice: once in a single-file SQLite database (sqlite-vec for vector
search, FTS5 for full-text), and once as a markdown file with F3.1 frontmatter
in the vault. The human edits the same markdown. The format is versioned and
documented in docs/f3.1-format.md.
# Install (PyPI):
pip install seahorse-memory
# …or with uv:
uv tool install seahorse-memory
# For hybrid semantic retrieval (FastEmbed ONNX, downloads mE5-small on first
# embed): pip install "seahorse-memory[embeddings]"
# For the multi-LLM extraction path (LiteLLM): pip install "seahorse-memory[llm]"
# Create a vault and write your first episode:
seahorse init myvault
seahorse remember "Sergio lives in Madrid" --title home
seahorse recall "madrid"
# Improve and forget (append-only; history is preserved):
seahorse improve <ep_id> "Sergio lives in Barcelona" --reason correction
seahorse forget <ep_id> --reason done
# Session capture, context, and consolidation:
# Install the observer (writes [observe] + merges the Claude Code hooks into
# ~/.claude/settings.json):
seahorse setup
# Start the observer (unix socket + worker), then the next session is captured
# automatically (skip-first, redacted, deterministic summary):
seahorse observe start
seahorse observe status
# Bootstrap context by recency (the SessionStart hook injects this):
seahorse context
# Distill recurrent episodes into semantic knowledge notes (N≥3, idempotent):
seahorse consolidate
# Remove the observer:
seahorse setup --uninstall
# Serve an agent over stdio MCP (io.seahorse.memory/v1):
seahorse-mcp --vault myvault
# …equivalently:
seahorse mcp --vault myvault
The seahorse console script is for humans and shell scripts; seahorse-mcp is
for agents. The seahorse mcp subcommand invokes the same stdio server as
seahorse-mcp, so both agent entry points are equivalent. To connect an agent,
see Use it from an agent.
sqlite3 must support enable_load_extension (sqlite-vec needs it); most
standard builds do — seahorse doctor reports it as a FAIL if not.seahorse init creates a .seahorse/ sidecar in a plain folder. Obsidian is
a human-facing editor for the same folder; its .obsidian/ directory is
ignored by Seahorse, never required.A vault of pre-existing Obsidian notes (no frontmatter, or legacy tags/
created frontmatter) is not yet in the canonical format — seahorse index rebuild fails honestly on those notes. seahorse frontmatter migrate converts
them:
# Preview: classify every note, write nothing (always exit 0):
seahorse frontmatter migrate --vault myvault --dry-run
# Apply: convert legacy notes, leave canonical notes untouched, refuse
# incompatible notes:
seahorse frontmatter migrate --vault myvault
# Rebuild the sidecar index from the converted notes:
seahorse index rebuild --vault myvault
Apply exits 97 when incompatible notes block a full migration — the manifest
summary is printed first so the operator sees which notes need manual
resolution. --resume skips notes unchanged since the last manifest;
--batch-size sets the manifest checkpoint cadence. Migration works before
seahorse init (it only touches .md files + the manifest).
First run: the semantic-embedding model (mE5-small, ~235MB) downloads lazily on the first
remember/recall— the CLI announces it so the first call doesn't look hung.seahorse statusshows the active retrieval regime (hybrid RRF (model cached)vscurrent-state listing — install seahorse-memory[embeddings] for semantic recall).
A comparison of verified facts, not a ranking. Sources: the project's state-of-the-art analysis (see the research notes and the claims cited below).
| Seahorse | mem0 | Letta / MemGPT | Zep / Graphiti | claude-mem | LangMem | |
|---|---|---|---|---|---|---|
| Portable open format | ✓ F3.1 spec | ✗ proprietary | ✗ runtime-bound | ✗ | ✗ own schema | ✗ |
| Human-readable layer | ✓ Obsidian vault | ✗ | ✗ | ✗ | ✗ | ✗ |
| Bi-temporal (point-in-time) | ✓ | ~ | ~ | ✓ Graphiti | ✗ | ✗ |
| Local-first, zero-infra | ✓ | ~ | ~ | ✗ cloud-only | ✓ | ~ |
| Reproducible benchmark | ✓ harness in-repo | ✗ #2800 | — | — | — | — |
| License | Apache-2.0 | Apache-2.0 (open-core) | Apache-2.0 | Apache-2.0 | AGPL | Apache-2.0 |
Legend: ✓ yes · ~ partial · ✗ no · — not verified.
The two facts that matter most: mem0 paywalls the features that produce its benchmark numbers, and Zep abandoned self-host for cloud-only. Seahorse is local-first by default, publishes its benchmark harness in the repo, and keeps the memory format portable so you are never locked in.
Seahorse ships a reproducible benchmark harness (LMEB-S, a subsample of the LongMemEval benchmark) and publishes its own numbers — with caveats. The point is not a leaderboard; it is an honest, reproducible measurement.
| Metric | Value | Note |
|---|---|---|
| recall@10 | 0.13 | knowledge-update slice: 0.44 |
| ndcg@10 | 0.11 | |
| mrr | 0.13 | knowledge-update slice: 0.47 |
| precision@10 | 0.02 | |
| token efficiency | 0.998 | 51.5M tokens full-context → 121K measured |
| latency p95 (INDEX) | 42 ms | retrieval-only, no rerank |
Caveats: the run uses a subsample (n≈470–500 questions, not the full dataset); relevance is judged by a small LLM without human validation; and it measures retrieval only, not the agent's final answer. A cross-encoder rerank was tested and rejected — it degraded recall@10 to 0.11 with 1.2s latency. Full methodology and reproduction commands in docs/benchmark.md.
These numbers measure retrieval ranking only on a subsample with a small judge — they are not comparable to the end-to-end accuracy scores other memory systems publish (e.g. Graphiti 63.8%, Mem0 94.8, Hindsight 91.4%). See docs/benchmark.md for how not to compare.
What is an episode? A single memory record: a markdown file with YAML
frontmatter carrying two time axes (valid_at — when it became true, and
created_at — when it was recorded), provenance, and a cognitive type. The
format is versioned and documented in docs/f3.1-format.md.
Why Obsidian? Because the human is part of the memory system. The agent writes into the same vault you edit — markdown is readable, diffable in git, and auditable. If the agent is wrong, you correct the note, not a database.
How is this different from claude-mem? claude-mem stores session
observations in its own schema. Seahorse is an open, bi-temporal standard with
a portable format and a human-readable layer — and seahorse import migrates
claude-mem observations into canonical episodes, so it is a bridge, not a
competitor.
Do I need an LLM? No. The deterministic skip-path is the default for the
bulk of writes (near-zero cost). LLM extraction is optional (seahorse-memory[llm])
and reserved for the few episodes that justify it.
Is it free? Yes. Apache-2.0, local-first, zero-infra. A managed SaaS and enterprise tier are planned for the future (see the project's strategy notes).
How do I contribute? See CONTRIBUTING.md for the development setup, test/lint commands, and pull request workflow.
See ROADMAP.md for what is built, what is next, and the direction of the project. Release history lives in CHANGELOG.md.
Exposed over stdio MCP (io.seahorse.memory/v1, protocol pinned 2025-11-25) and
mirrored on the CLI. These are memory primitives, not generic CRUD: an agent calls
remember / recall / improve / forget the way a human would talk about memory.
The 7 primitives (write + retrieve):
| Primitive | What it does |
|---|---|
remember | Record an episode (body, source, optional title/subject). |
recall | INDEX level — the current-state listing, clamped to top_k. |
recall_timeline | TIMELINE level — the supersedes chain around an anchor episode. |
recall_full | FULL level — the hydrated episode with all provenance. |
improve | Supersede an episode with a corrected one (append-only). |
forget | Soft-delete an episode (append-only; history preserved). |
build_pit | Build a point-in-time projection (all-None → current state). |
Plus 7 procedural / read-only tools (skills + facade introspection):
| Tool | What it does |
|---|---|
skill_add | Create a procedural skill (deterministic, near-zero cost). |
skill_show | Show a skill's gated body (trust gate). |
skill_list | List procedural skills (Discovery level). |
skill_search | Search procedural skills (hybrid recall, procedural filter). |
freshness_view | Freshness snapshot of an episode (age, stale, pending_ingest). |
audit_log | Audit events for an episode (write-path history). |
follow_supersedes_chain | The supersedes closure for an episode (version history). |
Three retrieval levels give progressive disclosure: a cheap listing first (INDEX), the chain on demand (TIMELINE), and the full record only when needed (FULL). This keeps the common path cheap.
sqlite3 + sqlite-vec (FTS5
recall ranks by relevance — sqlite-vec
kNN + FTS5 BM25 fused with Reciprocal Rank Fusion, with point-in-time
routing (state_at / known_at) when a real embedder is wired. The write
path and seahorse index rebuild populate vec0/FTS (best-effort — an
embedder failure never fails the episode write).embeddings extra (or with no vectors
populated), recall falls back to the current-state listing (score 0.0, no
ranking) and point-in-time recall is refused — the engine keeps working
without ranking.score' = score · 2^(-age/half_life)),
with per-type half-life priors. Off by default: the pure-RRF fingerprint stays
bit-comparable.seahorse init --llm bootstraps it;
the skip-path stays the near-zero-cost default for the bulk of writes.ollama/qwen3:0.6b) so the validator + repair
must carry the load — the path does not silently depend on native structured
outputs or a strong model.improve) and soft-delete (forget) with full history preserved.seahorse consolidate): distill many episodes into a
single consolidated note — deterministic by default, with opt-in LLM synthesis
(--synthesis llm) and supersession (--supersede) so the consolidated note
supersedes its sources.seahorse frontmatter migrate converts legacy
Obsidian notes with a --dry-run preview, --resume, and honest exit 97
when incompatible notes block a full migration.{"error": {...}} envelope on stderr, so
agents and scripts can branch on seahorse_code / cli_code deterministically.A few CLI commands are wired but intentionally return exit 75 with a reason
(expire, revalidate, index verify), so the surface is honest about what is
not implemented yet rather than silently no-op'ing. llm_partial stays fully
reserved.
sqlite3 + sqlite-vec for storage (zero-infra single
file; the vec0 virtual table + FTS5).Episode contract (core type system).seahorse.cli.seahorse.mcp package — import seahorse.mcp does not load Typer).ruamel.yaml + python-frontmatter, confined to the frontmatter adapter.embeddings extra, NOT in the default
install): the mE5-small bundle defaults to model_O4.onnx (fp32, ~235MB) — no
int8/fp16 artifact is portable to Apple Silicon, and an open standard must run
on Windows/Linux/macOS. A portable int8 bundle is a measured follow-up.llm extra, NOT in the default install): unifies the 100+
provider surface for the LLM extraction path. Without the extra, seahorse.llm
still imports (contract + StubLLMClient) and the real path degrades llm→skip
with a setup hint.The FastAPI / SQLAlchemy / Postgres stack is planned for a later multi-agent tier (Postgres + pgvector). The README states what ships now, not the target architecture.
uv run pytest (coverage ≥ 80% gate).scripts/e2e-fresh-user.sh — the full
install → init → core CLI → embeddings → LLM → import → MCP flow from a clean,
isolated HOME (never touches the real ~/.claude / ~/.claude-mem).scripts/e2e-matrix.sh — the fresh-user flow across
environment combinations (install method × extras × Obsidian × Ollama ×
online/offline × vault state × concurrency). --ci-subset runs the CI-safe
combos (core_min + uv_sync_dev); --list shows all combos.scripts/stress-core.sh — ingest 1000+ episodes, recall
--top-k 100 p95 ≤ 250ms (in-process INDEX budget), concurrent single-writer,
reindex, idempotent import, improve/forget chain.Contributions are welcome. See CONTRIBUTING.md for the development setup, test/lint commands, and pull request workflow. Release history lives in CHANGELOG.md.
Apache-2.0. See LICENSE.
v0.10.0. The memory engine works end-to-end from a clean install: write
episodes, recall them with hybrid semantic retrieval, extract with a real
multi-LLM path (local-first, CI-gated), improve and forget them, and serve an
agent over stdio MCP. Recall ranks by relevance when vectors are populated and
the embedder is wired, and honestly degrades to a current-state listing
otherwise. An opt-in decay ranking bias (default-off) downweights stale
knowledge by age. seahorse import migrates claude-mem observations to
episodes, and batch distillation (seahorse consolidate) turns many episodes
into one consolidated note, with opt-in LLM synthesis and supersession. The
benchmark harness ships in the repo with caveats and reproduction commands in
docs/benchmark.md. See What works and
ROADMAP.md for what is next.
272 commits
Python
98.1%
Shell
1.9%
Persistent, bi-temporal memory for LLM agents — local-first, MCP-native, Obsidian-readable.
pip install seahorse-memory
seahorse init myvault && seahorse remember "Sergio lives in Madrid"
seahorse recall "where does Sergio live?"
LLM agents start every session from zero. The context window is not memory: it is a scratchpad that resets, and it is too small to hold what an agent has learned across weeks of work. The tools that try to fix this have their own problems:
Seahorse is a different approach: an open, portable, bi-temporal memory standard that an agent writes to and reads from, that a human can read and correct, and that does not lock you into any runtime or provider.
The fastest way to see Seahorse is to give Claude Code a memory that survives between sessions. Three steps:
1. Capture sessions. seahorse setup installs the observer hooks into
~/.claude/settings.json; seahorse observe start runs the capture worker.
Every session is recorded as episodes — skip-first (near-zero cost),
redacted, with a deterministic summary.
seahorse setup
seahorse observe start
2. Recall across sessions. The SessionStart hook injects seahorse context
into the next session, so the agent starts with what it learned before. Ask
directly with seahorse recall:
seahorse context
seahorse recall "what did we decide about the API design?"
3. Bring your existing memory. If you already use claude-mem, seahorse import migrates its observations into canonical episodes — no replay, no
lock-in:
seahorse import --mode commit
The key difference: the agent writes into the same vault you edit in Obsidian. Every episode is a markdown file with YAML frontmatter — readable, editable, diffable in git, and auditable by a human. The agent's memory is not a black box; it is your notes.
Seahorse is built for agents: the memory surface is a stdio MCP server
(io.seahorse.memory/v1) that any agent that speaks MCP can connect to. The
CLI is for humans and scripts; agents talk to seahorse-mcp.
Register the server in Claude Code (local scope, default):
claude mcp add seahorse-mcp -- uvx --from seahorse-memory seahorse-mcp --vault "${HOME}/myvault"
The -- is required — it separates Claude's own flags from the server command.
Use --scope project to share the server with a team via .mcp.json (checked
into git). Verify with claude mcp list (should show ✔ Connected) and
claude mcp get seahorse-mcp.
Or configure it in .mcp.json at the project root (works with any MCP
client):
{
"mcpServers": {
"seahorse-mcp": {
"type": "stdio",
"command": "uvx",
"args": ["--from", "seahorse-memory", "seahorse-mcp", "--vault", "${HOME}/myvault"]
}
}
}
Note: ~ is not expanded in .mcp.json — use ${HOME} or an absolute path.
(mcpServers in settings.json is silently ignored; MCP servers live in
~/.claude.json for user/local scope and in .mcp.json for project scope.)
Once connected, the agent sees the 14 memory tools — remember, recall,
recall_timeline, recall_full, improve, forget, build_pit,
skill_add, skill_show, skill_list, skill_search, freshness_view,
audit_log, follow_supersedes_chain (see The agent
surface).
The observer (seahorse setup) is a separate piece: it captures Claude Code
sessions into episodes. The MCP server is how the agent reads and writes
memory. Both work together — capture sessions, then recall across them.
graph LR
A[Claude Code / any MCP agent] -- stdio MCP io.seahorse.memory/v1 --> S[seahorse-mcp]
S --> E[Bi-temporal engine]
E --> DB[(sqlite3 + sqlite-vec + FTS5)]
E --> V[Obsidian vault: markdown + F3.1 frontmatter]
H[Human in Obsidian] --> V
An agent talks to seahorse-mcp over stdio MCP. The engine stores every
episode twice: once in a single-file SQLite database (sqlite-vec for vector
search, FTS5 for full-text), and once as a markdown file with F3.1 frontmatter
in the vault. The human edits the same markdown. The format is versioned and
documented in docs/f3.1-format.md.
# Install (PyPI):
pip install seahorse-memory
# …or with uv:
uv tool install seahorse-memory
# For hybrid semantic retrieval (FastEmbed ONNX, downloads mE5-small on first
# embed): pip install "seahorse-memory[embeddings]"
# For the multi-LLM extraction path (LiteLLM): pip install "seahorse-memory[llm]"
# Create a vault and write your first episode:
seahorse init myvault
seahorse remember "Sergio lives in Madrid" --title home
seahorse recall "madrid"
# Improve and forget (append-only; history is preserved):
seahorse improve <ep_id> "Sergio lives in Barcelona" --reason correction
seahorse forget <ep_id> --reason done
# Session capture, context, and consolidation:
# Install the observer (writes [observe] + merges the Claude Code hooks into
# ~/.claude/settings.json):
seahorse setup
# Start the observer (unix socket + worker), then the next session is captured
# automatically (skip-first, redacted, deterministic summary):
seahorse observe start
seahorse observe status
# Bootstrap context by recency (the SessionStart hook injects this):
seahorse context
# Distill recurrent episodes into semantic knowledge notes (N≥3, idempotent):
seahorse consolidate
# Remove the observer:
seahorse setup --uninstall
# Serve an agent over stdio MCP (io.seahorse.memory/v1):
seahorse-mcp --vault myvault
# …equivalently:
seahorse mcp --vault myvault
The seahorse console script is for humans and shell scripts; seahorse-mcp is
for agents. The seahorse mcp subcommand invokes the same stdio server as
seahorse-mcp, so both agent entry points are equivalent. To connect an agent,
see Use it from an agent.
sqlite3 must support enable_load_extension (sqlite-vec needs it); most
standard builds do — seahorse doctor reports it as a FAIL if not.seahorse init creates a .seahorse/ sidecar in a plain folder. Obsidian is
a human-facing editor for the same folder; its .obsidian/ directory is
ignored by Seahorse, never required.A vault of pre-existing Obsidian notes (no frontmatter, or legacy tags/
created frontmatter) is not yet in the canonical format — seahorse index rebuild fails honestly on those notes. seahorse frontmatter migrate converts
them:
# Preview: classify every note, write nothing (always exit 0):
seahorse frontmatter migrate --vault myvault --dry-run
# Apply: convert legacy notes, leave canonical notes untouched, refuse
# incompatible notes:
seahorse frontmatter migrate --vault myvault
# Rebuild the sidecar index from the converted notes:
seahorse index rebuild --vault myvault
Apply exits 97 when incompatible notes block a full migration — the manifest
summary is printed first so the operator sees which notes need manual
resolution. --resume skips notes unchanged since the last manifest;
--batch-size sets the manifest checkpoint cadence. Migration works before
seahorse init (it only touches .md files + the manifest).
First run: the semantic-embedding model (mE5-small, ~235MB) downloads lazily on the first
remember/recall— the CLI announces it so the first call doesn't look hung.seahorse statusshows the active retrieval regime (hybrid RRF (model cached)vscurrent-state listing — install seahorse-memory[embeddings] for semantic recall).
A comparison of verified facts, not a ranking. Sources: the project's state-of-the-art analysis (see the research notes and the claims cited below).
| Seahorse | mem0 | Letta / MemGPT | Zep / Graphiti | claude-mem | LangMem | |
|---|---|---|---|---|---|---|
| Portable open format | ✓ F3.1 spec | ✗ proprietary | ✗ runtime-bound | ✗ | ✗ own schema | ✗ |
| Human-readable layer | ✓ Obsidian vault | ✗ | ✗ | ✗ | ✗ | ✗ |
| Bi-temporal (point-in-time) | ✓ | ~ | ~ | ✓ Graphiti | ✗ | ✗ |
| Local-first, zero-infra | ✓ | ~ | ~ | ✗ cloud-only | ✓ | ~ |
| Reproducible benchmark | ✓ harness in-repo | ✗ #2800 | — | — | — | — |
| License | Apache-2.0 | Apache-2.0 (open-core) | Apache-2.0 | Apache-2.0 | AGPL | Apache-2.0 |
Legend: ✓ yes · ~ partial · ✗ no · — not verified.
The two facts that matter most: mem0 paywalls the features that produce its benchmark numbers, and Zep abandoned self-host for cloud-only. Seahorse is local-first by default, publishes its benchmark harness in the repo, and keeps the memory format portable so you are never locked in.
Seahorse ships a reproducible benchmark harness (LMEB-S, a subsample of the LongMemEval benchmark) and publishes its own numbers — with caveats. The point is not a leaderboard; it is an honest, reproducible measurement.
| Metric | Value | Note |
|---|---|---|
| recall@10 | 0.13 | knowledge-update slice: 0.44 |
| ndcg@10 | 0.11 | |
| mrr | 0.13 | knowledge-update slice: 0.47 |
| precision@10 | 0.02 | |
| token efficiency | 0.998 | 51.5M tokens full-context → 121K measured |
| latency p95 (INDEX) | 42 ms | retrieval-only, no rerank |
Caveats: the run uses a subsample (n≈470–500 questions, not the full dataset); relevance is judged by a small LLM without human validation; and it measures retrieval only, not the agent's final answer. A cross-encoder rerank was tested and rejected — it degraded recall@10 to 0.11 with 1.2s latency. Full methodology and reproduction commands in docs/benchmark.md.
These numbers measure retrieval ranking only on a subsample with a small judge — they are not comparable to the end-to-end accuracy scores other memory systems publish (e.g. Graphiti 63.8%, Mem0 94.8, Hindsight 91.4%). See docs/benchmark.md for how not to compare.
What is an episode? A single memory record: a markdown file with YAML
frontmatter carrying two time axes (valid_at — when it became true, and
created_at — when it was recorded), provenance, and a cognitive type. The
format is versioned and documented in docs/f3.1-format.md.
Why Obsidian? Because the human is part of the memory system. The agent writes into the same vault you edit — markdown is readable, diffable in git, and auditable. If the agent is wrong, you correct the note, not a database.
How is this different from claude-mem? claude-mem stores session
observations in its own schema. Seahorse is an open, bi-temporal standard with
a portable format and a human-readable layer — and seahorse import migrates
claude-mem observations into canonical episodes, so it is a bridge, not a
competitor.
Do I need an LLM? No. The deterministic skip-path is the default for the
bulk of writes (near-zero cost). LLM extraction is optional (seahorse-memory[llm])
and reserved for the few episodes that justify it.
Is it free? Yes. Apache-2.0, local-first, zero-infra. A managed SaaS and enterprise tier are planned for the future (see the project's strategy notes).
How do I contribute? See CONTRIBUTING.md for the development setup, test/lint commands, and pull request workflow.
See ROADMAP.md for what is built, what is next, and the direction of the project. Release history lives in CHANGELOG.md.
Exposed over stdio MCP (io.seahorse.memory/v1, protocol pinned 2025-11-25) and
mirrored on the CLI. These are memory primitives, not generic CRUD: an agent calls
remember / recall / improve / forget the way a human would talk about memory.
The 7 primitives (write + retrieve):
| Primitive | What it does |
|---|---|
remember | Record an episode (body, source, optional title/subject). |
recall | INDEX level — the current-state listing, clamped to top_k. |
recall_timeline | TIMELINE level — the supersedes chain around an anchor episode. |
recall_full | FULL level — the hydrated episode with all provenance. |
improve | Supersede an episode with a corrected one (append-only). |
forget | Soft-delete an episode (append-only; history preserved). |
build_pit | Build a point-in-time projection (all-None → current state). |
Plus 7 procedural / read-only tools (skills + facade introspection):
| Tool | What it does |
|---|---|
skill_add | Create a procedural skill (deterministic, near-zero cost). |
skill_show | Show a skill's gated body (trust gate). |
skill_list | List procedural skills (Discovery level). |
skill_search | Search procedural skills (hybrid recall, procedural filter). |
freshness_view | Freshness snapshot of an episode (age, stale, pending_ingest). |
audit_log | Audit events for an episode (write-path history). |
follow_supersedes_chain | The supersedes closure for an episode (version history). |
Three retrieval levels give progressive disclosure: a cheap listing first (INDEX), the chain on demand (TIMELINE), and the full record only when needed (FULL). This keeps the common path cheap.
sqlite3 + sqlite-vec (FTS5
recall ranks by relevance — sqlite-vec
kNN + FTS5 BM25 fused with Reciprocal Rank Fusion, with point-in-time
routing (state_at / known_at) when a real embedder is wired. The write
path and seahorse index rebuild populate vec0/FTS (best-effort — an
embedder failure never fails the episode write).embeddings extra (or with no vectors
populated), recall falls back to the current-state listing (score 0.0, no
ranking) and point-in-time recall is refused — the engine keeps working
without ranking.score' = score · 2^(-age/half_life)),
with per-type half-life priors. Off by default: the pure-RRF fingerprint stays
bit-comparable.seahorse init --llm bootstraps it;
the skip-path stays the near-zero-cost default for the bulk of writes.ollama/qwen3:0.6b) so the validator + repair
must carry the load — the path does not silently depend on native structured
outputs or a strong model.improve) and soft-delete (forget) with full history preserved.seahorse consolidate): distill many episodes into a
single consolidated note — deterministic by default, with opt-in LLM synthesis
(--synthesis llm) and supersession (--supersede) so the consolidated note
supersedes its sources.seahorse frontmatter migrate converts legacy
Obsidian notes with a --dry-run preview, --resume, and honest exit 97
when incompatible notes block a full migration.{"error": {...}} envelope on stderr, so
agents and scripts can branch on seahorse_code / cli_code deterministically.A few CLI commands are wired but intentionally return exit 75 with a reason
(expire, revalidate, index verify), so the surface is honest about what is
not implemented yet rather than silently no-op'ing. llm_partial stays fully
reserved.
sqlite3 + sqlite-vec for storage (zero-infra single
file; the vec0 virtual table + FTS5).Episode contract (core type system).seahorse.cli.seahorse.mcp package — import seahorse.mcp does not load Typer).ruamel.yaml + python-frontmatter, confined to the frontmatter adapter.embeddings extra, NOT in the default
install): the mE5-small bundle defaults to model_O4.onnx (fp32, ~235MB) — no
int8/fp16 artifact is portable to Apple Silicon, and an open standard must run
on Windows/Linux/macOS. A portable int8 bundle is a measured follow-up.llm extra, NOT in the default install): unifies the 100+
provider surface for the LLM extraction path. Without the extra, seahorse.llm
still imports (contract + StubLLMClient) and the real path degrades llm→skip
with a setup hint.The FastAPI / SQLAlchemy / Postgres stack is planned for a later multi-agent tier (Postgres + pgvector). The README states what ships now, not the target architecture.
uv run pytest (coverage ≥ 80% gate).scripts/e2e-fresh-user.sh — the full
install → init → core CLI → embeddings → LLM → import → MCP flow from a clean,
isolated HOME (never touches the real ~/.claude / ~/.claude-mem).scripts/e2e-matrix.sh — the fresh-user flow across
environment combinations (install method × extras × Obsidian × Ollama ×
online/offline × vault state × concurrency). --ci-subset runs the CI-safe
combos (core_min + uv_sync_dev); --list shows all combos.scripts/stress-core.sh — ingest 1000+ episodes, recall
--top-k 100 p95 ≤ 250ms (in-process INDEX budget), concurrent single-writer,
reindex, idempotent import, improve/forget chain.Contributions are welcome. See CONTRIBUTING.md for the development setup, test/lint commands, and pull request workflow. Release history lives in CHANGELOG.md.
Apache-2.0. See LICENSE.
v0.10.0. The memory engine works end-to-end from a clean install: write
episodes, recall them with hybrid semantic retrieval, extract with a real
multi-LLM path (local-first, CI-gated), improve and forget them, and serve an
agent over stdio MCP. Recall ranks by relevance when vectors are populated and
the embedder is wired, and honestly degrades to a current-state listing
otherwise. An opt-in decay ranking bias (default-off) downweights stale
knowledge by age. seahorse import migrates claude-mem observations to
episodes, and batch distillation (seahorse consolidate) turns many episodes
into one consolidated note, with opt-in LLM synthesis and supersession. The
benchmark harness ships in the repo with caveats and reproduction commands in
docs/benchmark.md. See What works and
ROADMAP.md for what is next.
272 commits
Python
98.1%
Shell
1.9%