Persistent long-term memory for Claude Code — cloud-native with SQLite + Modal
Project description
yaucca
Yet Another Useless Claude Code Agent — persistent long-term memory for Claude Code, deployed as a cloud-native FastAPI server on Modal.com.
Every Claude Code session starts with full memory context and ends by persisting what happened. Memory survives across sessions, projects, and context compactions.
Architecture
┌──────────────────────────────────────────────────────┐
│ Modal.com (scale-to-zero, ~$0-1/month) │
│ │
│ FastAPI + SQLite + sqlite-vec + Qwen3 embeddings │
│ Remote MCP (OAuth 2.1 + GitHub login) │
│ Persistent volume: /data/yaucca.db │
└────────────────┬──────────────────────────────────────┘
│ HTTPS
┌────────────┼────────────┐
│ │ │
┌───┴────┐ ┌────┴───┐ ┌─────┴────┐
│ Claude │ │ Claude │ │ Claude │
│ Code │ │ .ai │ │ mobile │
│(laptop)│ │ (web) │ │ (phone) │
│ │ │ │ │ │
│ hooks │ │ remote │ │ remote │
│+remote │ │ MCP │ │ MCP │
│ MCP │ │ │ │ │
└────────┘ └────────┘ └──────────┘
Memory Tiers
- Core Memory (5 blocks, always loaded):
user,projects,patterns,learnings,context - Archival Memory (searchable): Long-term storage with Qwen3-Embedding-8B semantic vector search (1024 dims via OpenRouter)
- Recall Memory (pre-loaded): Recent conversation history injected at startup
Quick Start
Step 1: Deploy your backend (once, from your laptop)
# Install with deployment deps
uv pip install yaucca[deploy]
# Authenticate with Modal (one-time)
modal setup
# Create a GitHub OAuth App at https://github.com/settings/developers
# Homepage URL: https://<your-username>--yaucca-serve.modal.run
# Callback URL: https://<your-username>--yaucca-serve.modal.run/oauth/github/callback
# Create .env with your credentials
cat > .env << 'EOF'
YAUCCA_URL=https://<your-username>--yaucca-serve.modal.run
YAUCCA_AUTH_TOKEN=<generate-with: python3 -c "import secrets; print(secrets.token_urlsafe(32))">
OPENROUTER_API_KEY=<your-openrouter-key>
YAUCCA_ISSUER_URL=https://<your-username>--yaucca-serve.modal.run
GITHUB_CLIENT_ID=<from-github-oauth-app>
GITHUB_CLIENT_SECRET=<from-github-oauth-app>
GITHUB_ALLOWED_USERS=<your-github-username>
EOF
# Push secrets to Modal and deploy
yaucca-deploy-secrets
modal deploy src/yaucca/cloud/modal_app.py
# Verify
curl https://<your-username>--yaucca-serve.modal.run/health
Step 2: Use it everywhere
On every machine or cloud environment where you use Claude Code:
# Install the client (hooks only — lightweight)
uv pip install yaucca
# Install hooks into ~/.claude/settings.json
yaucca-install
# Add the remote MCP server
claude mcp add --transport http -s project yaucca \
https://<your-username>--yaucca-serve.modal.run/mcp
First-time MCP auth: Claude Code will show ! Needs authentication
for the yaucca server. To connect:
- Type
/mcpin the Claude Code prompt - Select yaucca → browser opens → GitHub login → authorize
- "Authentication successful. Connected to yaucca."
- All 7 memory tools are now available
The OAuth token is cached and refreshed automatically.
Hook lifecycle
- SessionStart: Injects memory context (core blocks + recent exchanges)
- Stop (every turn): Persists raw exchanges to archival — cheap HTTP POSTs, no LLM calls
- SessionEnd (on exit): Single
claude -pcall generates both an archival summary and an updated context block
Rollback
# If hooks are broken, uninstall them
yaucca-install --uninstall
# If MCP is broken, remove it
claude mcp remove -s project yaucca
# Restore a backup of settings.json
cp ~/.claude/settings.json.bak ~/.claude/settings.json
The SQLite database on Modal's persistent volume is never modified by rollback — your memory is always safe.
Verify
# Test SessionStart hook — should print XML memory context to stdout
echo '{"source":"startup"}' | python -m yaucca.hooks session_start
# If it prints nothing, check stderr for errors.
# First run may be slow (~5s) due to Modal cold start.
# Open Claude Code — it should load your memory context
claude
Testing the Cutover from Letta
If you're migrating from v1 (Letta-based), follow this process. Your Letta data is untouched throughout — this is a copy, not a move.
Step 1: Migrate data
Make sure .env has both the cloud config and the Letta config:
# .env should contain:
# YAUCCA_URL=https://<url>.modal.run
# YAUCCA_AUTH_TOKEN=<token>
# LETTA_BASE_URL=http://localhost:8283
# YAUCCA_AGENT_ID=<your-letta-agent-id>
uv run --extra migrate python -m yaucca.cloud.migrate
Safe to re-run — deduplicates by text content.
Step 2: Verify cloud data
# source .env for curl commands
source .env
# Check blocks
curl -s $YAUCCA_URL/api/blocks \
-H "Authorization: Bearer $YAUCCA_AUTH_TOKEN" | python3 -m json.tool
# Check passage count
curl -s "$YAUCCA_URL/api/passages?limit=1000" \
-H "Authorization: Bearer $YAUCCA_AUTH_TOKEN" | python3 -c \
"import json,sys; print(f'{len(json.load(sys.stdin))} passages')"
# Test vector search
curl -s "$YAUCCA_URL/api/passages/search?q=test+query" \
-H "Authorization: Bearer $YAUCCA_AUTH_TOKEN" | python3 -m json.tool
# Health (includes vec status)
curl -s $YAUCCA_URL/health \
-H "Authorization: Bearer $YAUCCA_AUTH_TOKEN" | python3 -m json.tool
Step 3: Test hooks locally (without changing your live config)
cd /path/to/yaucca
# Test SessionStart — should print XML memory from the cloud
echo '{"source":"startup"}' | uv run python -m yaucca.hooks session_start
# Check passage stats
uv run python -m yaucca.hooks status
Step 4: Switch over
cd /path/to/yaucca
uv run python -m yaucca.install
This replaces any existing yaucca hooks with ones pointing at this repo.
A backup is saved to ~/.claude/settings.json.bak.
Step 5: Rollback if something breaks
The old Letta-based system is still intact. To revert:
# Option A: uninstall yaucca hooks entirely
cd /path/to/yaucca
uv run python -m yaucca.install --uninstall
# Then re-install the old Letta-based hooks from the old repo
cd /path/to/old/yetanotheruseless_claude_code_agent
uv run python -m yaucca.install
# Option B: just restore the backup
cp ~/.claude/settings.json.bak ~/.claude/settings.json
Verify Letta is still running: curl http://localhost:8283/v1/health
No data is lost — Letta still has all your original memory. Any new data written to the cloud during testing is simply extra; it won't conflict.
Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
YAUCCA_URL |
(fails fast if unset) | Cloud server URL |
YAUCCA_AUTH_TOKEN |
(none) | Bearer token for cloud API |
YAUCCA_REQUIRED |
false |
If true, hooks exit non-zero when cloud is unreachable |
YAUCCA_EMBED_BASE_URL |
https://openrouter.ai/api/v1 |
Embedding API base URL |
YAUCCA_EMBED_MODEL |
qwen/qwen3-embedding-8b |
Embedding model |
YAUCCA_EMBED_DIMS |
1024 |
Embedding dimensions |
Stateful Agent Mode
Set YAUCCA_REQUIRED=true when running yaucca as a critical dependency (e.g.
for a stateful agent that must have memory). In this mode, hooks will fail hard
(exit 1) if the cloud is unreachable, rather than silently starting without
memory. A stateful agent without memory is a different, broken thing — not a
gracefully degraded version of the same thing.
Embedding Model Comparison
yaucca supports multiple embedding profiles for A/B testing retrieval quality across different models or Matryoshka dimension truncations.
How It Works
Each embedding profile creates a separate passages_vec_{name} table in SQLite.
When a passage is inserted, its embedding is truncated to each profile's
dimension and stored in every active profile's table. Search can target a
specific profile via ?profile= query parameter.
Side-by-Side Comparison
1. Configure multiple profiles in modal_app.py (or wherever you create
the Database):
from yaucca.cloud.db import Database, EmbeddingProfile
db = Database(
db_path="/data/yaucca.db",
embedding_profiles=[
EmbeddingProfile("d1024", 1024), # full Qwen3-Embedding-8B
EmbeddingProfile("d512", 512), # Matryoshka half
],
)
New passages will be indexed into both profiles automatically.
2. Backfill existing data. If you already have passages stored, the new profile's vec table will be empty — you must re-embed all existing passages before the comparison is valid. Use the built-in backfill endpoint or CLI:
# Via the server endpoint (re-embeds server-side, batched):
curl -X POST "$YAUCCA_URL/api/admin/backfill?profile=d512" \
-H "Authorization: Bearer $YAUCCA_AUTH_TOKEN"
# Or backfill all profiles at once:
curl -X POST "$YAUCCA_URL/api/admin/backfill" \
-H "Authorization: Bearer $YAUCCA_AUTH_TOKEN"
# Or via CLI (calls the endpoint):
uv run python -m yaucca.cloud.backfill --profile d512
The backfill embeds in batches of 50 for efficiency. It's idempotent — already-indexed passages are skipped.
3. Compare search results:
# Search with full 1024-dim profile
curl "$YAUCCA_URL/api/passages/search?q=authentication+bug&profile=d1024"
# Search with 512-dim profile
curl "$YAUCCA_URL/api/passages/search?q=authentication+bug&profile=d512"
4. Drop the losing profile once you've decided:
db.drop_profile("d512") # removes the vec table entirely
Comparing Different Models
To compare two entirely different embedding models (not just Matryoshka truncations of the same model):
- Create a profile with a distinct name and the new model's native dimension
(e.g.,
EmbeddingProfile("openai_1536", 1536)) - Update the server's embedder config to the new model
- Fully backfill that profile — every passage must be embedded with the new model. You cannot mix embeddings from different models in the same vec table; cosine similarity between vectors from different embedding spaces is meaningless
- Compare search results, then drop the loser's profile
Development
git clone https://github.com/jakemannix/yaucca.git
cd yaucca
uv sync --extra dev # Install all deps (client + server + test)
uv run pytest # Unit tests (114 tests)
uv run ruff check . && ruff format . # Lint + format
uv run mypy src/yaucca # Type check
License
Apache-2.0
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file yaucca-0.1.0.tar.gz.
File metadata
- Download URL: yaucca-0.1.0.tar.gz
- Upload date:
- Size: 180.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff35d4bceb754c172645bf554bd5b8a94cb371722bfd2d0e5f207bff87b81600
|
|
| MD5 |
7524fd9728456a59efb771595ae5f066
|
|
| BLAKE2b-256 |
37c3bb54edaab580686549158ccf45a6f7b2fe6ff73e5039ae0f148825d6b2d2
|
Provenance
The following attestation bundles were made for yaucca-0.1.0.tar.gz:
Publisher:
publish.yml on jakemannix/yaucca
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
yaucca-0.1.0.tar.gz -
Subject digest:
ff35d4bceb754c172645bf554bd5b8a94cb371722bfd2d0e5f207bff87b81600 - Sigstore transparency entry: 1171072928
- Sigstore integration time:
-
Permalink:
jakemannix/yaucca@1df296b4cb8a416484253a299efc384456484a10 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/jakemannix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1df296b4cb8a416484253a299efc384456484a10 -
Trigger Event:
release
-
Statement type:
File details
Details for the file yaucca-0.1.0-py3-none-any.whl.
File metadata
- Download URL: yaucca-0.1.0-py3-none-any.whl
- Upload date:
- Size: 41.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fce64283f6db0077b735cb79ede86a4a6b8fcdec27c7e967d793bf69d7592fb3
|
|
| MD5 |
3f8fad139c42a15f76a06d90c843a1a2
|
|
| BLAKE2b-256 |
afd82c658ce1422162959604824877c6107659dca13526a883c7d8f3f69c6a11
|
Provenance
The following attestation bundles were made for yaucca-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on jakemannix/yaucca
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
yaucca-0.1.0-py3-none-any.whl -
Subject digest:
fce64283f6db0077b735cb79ede86a4a6b8fcdec27c7e967d793bf69d7592fb3 - Sigstore transparency entry: 1171072992
- Sigstore integration time:
-
Permalink:
jakemannix/yaucca@1df296b4cb8a416484253a299efc384456484a10 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/jakemannix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1df296b4cb8a416484253a299efc384456484a10 -
Trigger Event:
release
-
Statement type: