memo
CLAUDE.md25 starsChanged 6 months agoMITView on GitHub
memo is a local-first semantic memory: MLX embeddings + sqlite-vec hybrid (vec + BM25) search, reranking, a knowledge graph, temporal reasoning, an MCP server, and a CLI. Single-user, runs offline. PyPI distribution name is mlx-memo; CLI binary is memo; MCP server binary is memo-mcp. If memo doctor warns memo Python package loaded from .../src/memo/..., outside the isolated runtime, a stray PYTHONPATH=src (relative to a ~/repos/memo cwd) is shadowing the isolated uv tool install with this working tree's source — memo…
- Commits and pushes
In this repository4 other files it gives its agents.
- remotion-best-practicesSkill
- remotion-createSkill
- remotion-renderSkill
- memoSkill
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
15 rules. About 0 are common, 3 are distinctive, and 12 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 35
If memo doctor warns memo Python package loaded from .../src/memo/..., outside the isolated runtime, a stray PYTHONPATH=src (relative to a …
- Very specific: line 89
LOCAL (intra-machine): all sessions on a Mac share one data_dir/memvec.db (WAL, thread-local conns), so a save/capture is visible to …
- Very specific: line 140
When stubbing MLXEmbedder.embed in tests, also pin MEMO_EMBEDDER_DIMS to the stub's output dim.
- Very specific: line 247
Rebuild, don't
rm: Use memo reindex --rebuild (not rm memvec.db) to rebuild from disk. - Very specific: line 279
Use the tmp_cfg fixture or build an isolated Config — never call Config.from_env() without controlling the environment.
- Distinctive: line 285
Never read or write the developer's real vault.
- Distinctive: line 375
Rule: when a search returns wrong results, do NOT patch that one query.
- Very specific: line 453
Not yet fixed: both are ranking/rendering changes and must go through memo eval recall --gate (the pre-push discipline above), not a quick …
- Distinctive: line 522
Treat explore output as Read-equivalent: don't re-verify with grep, don't delegate to file-reading subagents.
- Very specific: line 536
Pre-refactor gate: run scripts/cg_impact_gate.sh <symbol> before renaming or changing signatures under src/memo/.
- Very specific: line 557
Use /effort ultracode when a change benefits from parallel in-repo implementation and review.
- Very specific: line 563
Before deciding or answering anything that prior work might already cover, consult memo FIRST:
- Very specific: line 565
Start with memo_unified_briefing (or memo_search / memo_ask) to pull durable facts, decisions, and preferences.
- Very specific: line 574
Write durable outcomes back with memo_save so the next session inherits them.
- Very specific: line 577
Keep it honest: when a surfaced memory is stale or contradicted, correct it instead of silently working around it — memo_invalidate closes …
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
memo is a local-first semantic memory: MLX embeddings + sqlite-vec hybrid
(vec + BM25) search, reranking, a knowledge graph, temporal reasoning, an MCP
server, and a CLI. Single-user, runs offline. PyPI distribution name is mlx-memo; CLI binary is memo; MCP server binary is memo-mcp.
Dev commands
# One-time setup (fresh clone/worktree — pytest is an optional-dependency
# extra, not a base dep, so plain `uv sync` alone leaves pytest missing)
uv sync --extra dev # pytest, ruff, mypy, hypothesis, etc.
uv sync --all-extras # + http/tantivy/multimodal/ocr, needed for those test modules
# Tests (no MLX needed — MLX tests skip automatically on non-Apple Silicon)
uv run --no-sync pytest tests/ # full suite
uv run --no-sync pytest tests/test_foo.py::test_bar -v # single test
uv run --no-sync pytest tests/ -m "not slow" # skip real-MLX-load tests (>1s each)
# Type checking
uv run --no-sync mypy src/memo/
# Lint + format
uv run --no-sync ruff check src/ && uv run --no-sync ruff format src/
# Run the CLI
uv run --no-sync memo <cmd>
# Validate MEMO_* flags (typos, unknown vars)
uv run --no-sync memo config validateIf memo doctor warns memo Python package loaded from .../src/memo/..., outside
the isolated runtime, a stray PYTHONPATH=src (relative to a ~/repos/memo
cwd) is shadowing the isolated uv tool install with this working tree's
source — memo --version looks right while the code that actually runs is
stale. unset PYTHONPATH before verifying a fix against the real installed
binary.
Working tree is shared (read before any git op)
This repo is frequently worked on by concurrent agent sessions sharing one
working tree and HEAD. Their commits land on whatever branch is checked out,
master advances underneath you, and a git checkout in another session moves
your HEAD too. So:
- Never
git add -A/git commit -a/ruff format src/— they sweep in other sessions' in-flight files. Stage explicit paths only:git add src/memo/foo.py tests/test_foo.py. Lint/format only your files. - Check
git reflog+git statusbefore anyreset/checkout— you may un-reference a concurrent commit or move a shared HEAD. - Cut releases from an isolated worktree, never
git checkout masterin the shared tree:git worktree add --detach /tmp/rel origin/master, cherry-pick your commit, bump versions + CHANGELOG, commit,git push origin HEAD:master, tag,git worktree remove --force. Checkgit ls-remote --tags origin vX.Y.Zfirst — a concurrent session may have taken your version number.masteris GitHub-branch-protected (10 required status checks, no direct push — verified:git push origin HEAD:masterfails withGH006: Protected branch update failed) — land changes viagh pr create+ merge, not a raw push, even from an isolated worktree with a clean fast-forward. - Nothing is lost on entanglement (commits live in
git reflog, uncommitted work stays in the tree). Rebuild a clean branch withgit checkout <branch> -- <only your files>(zsh does NOT word-split$VAR— list paths explicitly).
Architecture
Memory facade (src/memo/memory/facade.py) multiply-inherits eighteen operation mixins — _WriteOpsMixin, _UpdateOpsMixin, _RelationOpsMixin, _LifecycleOpsMixin, _DeleteOpsMixin, _GraphOpsMixin, _SearchOpsMixin, _SearchScoringMixin, _EvidenceOpsMixin, _OutcomeFeedbackOpsMixin, _AskOpsMixin, _ChatAskOpsMixin, _RerankOpsMixin, _RepoOpsMixin, _MaintainOpsMixin, _ConsolidateOpsMixin, _ReplayOpsMixin, _SecretOpsMixin — each in their own src/memo/memory/<op>_ops.py file. Module-level constants, prompts, and pure helpers are in src/memo/memory/record.py. Never import from a mixin directly; always go through Memory.
MCP server (src/memo/server.py) registers tools via build_server(). Each domain is a server_<domain>.py module that exports register(server, memory) — called once in build_server(). Adding a new MCP tool = create src/memo/server_<domain>.py + add one register call in server.py. The _MaintainOpsMixin method is directly callable from tests via mock_memory.<method>().
Storage (src/memo/store/) is a subpackage. VecStore in queries.py is the primary interface: one sqlite-vec DB file, thread-local connections (required for FastMCP HTTP worker threadpool). Writes use _tx() (BEGIN IMMEDIATE); vectors are packed float32 blobs; WAL mode + busy_timeout.
CLI is in src/memo/cli.py (entry-point wiring only) + src/memo/cli_<domain>.py files. Each domain file exports a Click command or group imported and registered in cli.py.
Flags (src/memo/flags.py) is the single registry for all MEMO_* env vars — it aggregates FlagSpecs defined in the per-domain flags_<group>.py modules (flags_recall/search/behavior/capture/ingest/graph/misc.py, base types in flags_base.py). Add a new flag in the matching flags_<group>.py. Use flag_bool/int/float/str(name) — never os.environ.get("MEMO_...") inline. memo config validate parses every set flag.
Cache (src/memo/cache.py): query embeddings use Memo's native
thread-safe LRU cache when MEMO_QUERY_CACHE_SIZE > 0. The stable path never
loads a cache implementation from another package.
Contracts and replay URIs (src/memo/contracts.py,
src/memo/memory/replay_ops.py): Memo owns its request/result contracts and
memo:// parser. Legacy field names are migration aliases only.
Sync — two explicit tiers (src/memo/sync_git.py, src/memo/identity.py):
- LOCAL (intra-machine): all sessions on a Mac share one
data_dir/memvec.db(WAL, thread-local conns), so a save/capture is visible to sibling sessions on their next recall with no git.sync_tier(cfg)→"local"when no git remote is configured. - REMOTE (inter-machine): the git
memo-syncremote is the only cross-machine channel.sync_tier(cfg)→"remote". ONE owner per machine:sync_once()takes anflockonstate_dir/.sync.lock(concurrent same-machine sessions skip — their writes ride the lock holder's push) and does pull-rebase-before-push so an advanced remote rebases instead of rejecting. Triggers: per-promptmemo sync auto(debounced viaMEMO_SYNC_PUSH_DEBOUNCE_S/MEMO_SYNC_PULL_INTERVAL_S, async hook) +memo sync onceon Stop. A failed push stampsstate_dir/sync_pendingand retries next trigger. The repo also carries per-machine embed-cache shards (embed_cache/<device_id>.json,sync_embed_cache.py,MEMO_SYNC_EMBED_CACHEdefault on): document/chunk embeddings of the durable corpus, imported intorepo_embedding_cachebefore the post-pull reindex — so memories saved on another Mac (andmemo sync bootstrapon a fresh one) index with ~zero local MLX embed calls. Derived data only; reference/vault tier never exports. - Identity (
identity.py): stablemachine_id(= persistedcfg.device_id, alsohistory.device_id) +hostname+session_id+terminal. Commits are attributed[<hostname>·<session>]; Memo owns and exposes the identity. - Durable capture:
memo capture-tick(per-prompt async, self-throttled byMEMO_CAPTURE_INTERVAL_S) captures NEW turns since a per-session watermark so a long session's insight reaches.mdbefore Stop, not only at Stop. memo sync status/memo doctorsurface the silent no-op (not a clone) and stranded commits. The legacy audit-log replay (sync.pySyncManager) is the--remote <path>fallback; the machineflockis the coordinator.
Memory types (src/memo/tiers.py): durable tier = decision, fact, bug, feedback, preference, note, manual, synthesis, procedure, failure_pattern; reference tier = reference (bulk-ingested vault chunks, excluded from auto-recall). synthesis memories are auto-generated by memo synthesize / MEMO_SYNTHESIS_ENABLED=1 memo maintain — cross-cluster inferred insights with synthesis_sources provenance in their extra frontmatter bag.
MLX invariants (do not violate)
The embedder/LLM path depends on four invariants. A diff touching
src/memo/embedder.py, src/memo/llm.py, src/memo/memory/ (any file), or anything
that calls .embed() / .chat() must preserve all four:
- Asymmetric retrieval prefix on QUERIES only. Query text gets
_QUERY_INSTRUCTION_PREFIX(seeembedder.embed_query); stored documents do not. Prefixing both (or neither) collapses cosine similarity — the model places prefixed and raw inputs in different regions of the space. MLXEmbedder.embed()takesSequence[str], never a barestr. A bare string is iterated as characters and silently produces garbage. Always wrap:embed([text]).MEMO_EMBEDDER_DIMSmust match the model. 1024 / 2560 / 4096 for the 0.6B / 4B / 8B Qwen3-Embedding models. Default model ismlx-community/Qwen3-Embedding-0.6B-4bit-DWQ(1024-dim). A mismatch corrupts the vec0 table or fails the dims-validation guard instore/queries.py.mlx/mlx-lmimports stay deferred (inside functions, not at module level) — module-level import drags the MLX runtime into every CLI invocation and blows the recall-hook cold-start budget.
When stubbing MLXEmbedder.embed in tests, also pin MEMO_EMBEDDER_DIMS to the
stub's output dim.
Recall hook (UserPromptSubmit) — 5s budget
memo recall-hook is wired into Claude Code's UserPromptSubmit hook
(wired by wire_recall_hook() in src/memo/cli_hooks.py, which writes the
entry into ~/.claude/settings.json — NOT hooks/hooks.json, which owns the
session/capture/sync/maintenance hooks only). End-to-end it must stay under ~5s. Cold MLX load is ~2s,
leaving ~3s for embed + vec search + format. The rerank candidate pool
auto-shrinks (and MEMO_RECALL_TOKEN_BUDGET caps output) so warm latency stays
under the timeout. Anything added to the hook path — src/memo/cli_hooks.py, the
recall-hook command in src/memo/cli_recall_hook.py, embedder.py, store/queries.py search —
must respect that budget. A warm memo-mcp HTTP/recall daemon serves embeds
over a socket (MEMO_EMBEDDER_VIA_DAEMON) to skip cold load.
Runtime
memo and memo-mcp must resolve from the same isolated runtime
(pipx / uv tool / Homebrew) — not a project .venv. A mixed runtime is the
usual cause of "works in CLI, broken in MCP". memo doctor --strict-runtime
checks this.
Runtime plumbing lives in src/memo/runtime/: install.py (runtime/install
detection + memo doctor/install helpers), daemon.py (launchctl + warm
recall-daemon lifecycle, MEMO_EMBEDDER_VIA_DAEMON socket), report.py
(install-report presentation). These were extracted from cli.py/cli_runtime;
the warm memo-mcp daemon they manage is what serves embeds over the socket to
keep the recall hook under its 5s budget.
Chat — native RAG chat UI (:8765)
memo chat serve [--port 8765] [--dist web-chat/dist] serves the chat SPA
(vendored from the archived synapse chat) plus its API on 127.0.0.1:<port>
(requires the [http] extra — FastAPI/uvicorn; raises a ClickException
otherwise, not an ImportError — cli_chat.py catches the missing-extra
ImportError and re-raises it as a clean CLI error). Pipeline:
src/memo/chat/pipeline.py orchestrates retrieval (Memory.search /
Memory.repo_search, called directly — not through _ChatAskOpsMixin) →
dedup/fusion/fulldoc/rewrite → synthesis, streamed as SSE events
(stage/context/token/done/error) over POST /api/ask/stream.
Feedback closes the loop (src/memo/chat/feedback.py): per-turn 👍/👎 and
per-source votes boost a source's rank on an exact question-key match, or
(semantic fallback) cosine similarity ≥ MEMO_CHAT_SEMANTIC_THRESHOLD between
the vote's query embedding and the new query.
Knobs (src/memo/chat/config.py — env-only + built-in defaults, read directly
via os.environ; NOT registered in flags.py's markdown-config/tuned-overlay
chain, though the 11 names are excluded from flags.unknown_memo_vars so
memo config validate doesn't flag them as typos):
MEMO_CHAT_BASE_K (20, retrieval pool before dedup/rerank),
MEMO_CHAT_RELEVANCE_FLOOR (0.25), MEMO_CHAT_VOTE_BOOST (1.5, multiplier on
a 👍-voted source), MEMO_CHAT_SEMANTIC_THRESHOLD (0.75),
MEMO_CHAT_MULTI_QUERY (true) / MEMO_CHAT_MULTI_QUERY_N (2, query
rewrite/expansion), MEMO_CHAT_FULLDOC (true, inline the full doc for a
dominant source group), MEMO_CHAT_ANSWER_MAX_TOKENS (1200),
MEMO_CHAT_SYNTH_HEAD (8, sources actually fed to synthesis).
memo eval chat runs eval/chat_regression_corpus.json (schema
synapse.eval_chat.query.v1, rescued from synapse) through the pipeline and
checks pass/fail + p50/p95 latency — the chat-side counterpart to memo eval
recall (see Retrieval-regression discipline below).
Latency is retrieval-dominated, and multi-query is the biggest term.
Measured 2026-08-05 against the live corpus (11.3k memories, 4B embedder,
30B synthesiser): retrieval ~15-17s warm, ~70s on the service's first query
(the chat process cold-loads the LLM the query rewrite needs). memo chat ask
in a fresh process: 77s default, 31s with MEMO_CHAT_MULTI_QUERY=false. Turn
it off when latency matters more than recall breadth; keep it on for the UI,
where the SPA streams stages while retrieval runs.
Ops (launchd): memo ops install chat [--port 8765] [--dist <path>] /
memo ops uninstall chat / memo ops status (src/memo/ops_launchd.py)
render and bootstrap a com.memo.chat LaunchAgent (KeepAlive, logs to
~/Library/Logs/memo/chat.log). Post-release install uses the released
binary, not a worktree venv: uv tool upgrade mlx-memo && memo ops install
chat --dist <dist> — the plist's ProgramArguments point at whatever memo
resolves to in PATH at install time, so bootstrapping before the chat code
ships crash-loops under KeepAlive.
BM25 / Spanish search
FTS5's tokenizer wraps each \w+ token in its own phrase quotes, so a
multi-token query becomes an AND-of-tokens (not a phrase match). store/bm25_queries.py
tokenizes, AND-joins, and falls back to OR only on zero recall. Diacritics are
folded (unicode61 remove_diacritics 2) so "decision" matches "decisión".
Storage
VecStore (src/memo/store/queries.py) is sqlite-vec backed, one DB file, one
connection per thread (thread-local — required for the FastMCP HTTP transport's
worker threadpool). Writes go through _tx() (BEGIN IMMEDIATE); vectors are
packed float32 blobs; WAL mode + busy_timeout give concurrent readers + a
writer.
Markdown is the source of truth; sqlite is a rebuildable index
The .md files are canonical; the sqlite index is derived and replayable:
- Authority.
save()writes the.mdfirst, then indexes — if indexing fails, the memory is stamped_memo_embed_pendingon disk andmemo reindexreplays it (the save never silently vanishes).delete()drops the derived index first and removes the canonical.mdlast, rolling the store back (StorageError) if the unlink fails — so the truth-bearing file is never lost to a partial delete (a leftover index row is recoverable viareindex). A hand-edit in Obsidian wins on the nextreindex(body_hash mismatch → disk overwrites the index). - Rebuild, don't
rm. Usememo reindex --rebuild(notrm memvec.db) to rebuild from disk. It truncates only the markdown-derivable tables (meta/vec/fts) and preserves the user-signal tables —access,memory_health,source_feedback*— which are PRIMARY data not present in markdown and re-join on the stable memoryid. A content-addressed embedding cache (repo_embedding_cache, keyed onmodel+dims+sha256(text)) makes a warm rebuild issue ~zero embedder calls. - Memories in the vault.
MEMO_MEMORIES_IN_VAULT=1(needsMEMO_VAULT_PATH) stores memories under<vault>/<SYSTEM_DIR>/AI/memoryso the human-editable Obsidian vault is the source of truth. Ingest already excludesAI/and anyid:-frontmatter file, so they're never double-ingested as reference tier.memo migrate --into-vaultmoves an existing install there (non-destructive,--rollbackrestores the prior config).memo migratenever dropsmemvec.db. - One DB file (opt-in).
MEMO_SINGLE_DB=1folds the sidecar stores (history/graph/contradictions/crossref/fact_edges) intomemvec.db— each keeps its own connection to the one file (WAL allows it; no shared-transaction risk). Runmemo migrate --consolidate-dbonce to merge existing*.dbfiles (renames them*.db.bak, idempotent). Default off keeps the historical multi-file layout. Thecfg.*_dbpath properties collapse ontodb_pathwhen on. - Per-project folders (default on).
MEMO_STORE_BY_PROJECT=1writes each new memory's.mdinto a per-project bucket —memory_dir/<project>/(or_global/when untagged) — derived from theproject:tag (the tag stays the source of truth; the folder is derived). The sqlite index globs recursively, so search stays global — on-disk organization only. Recall then applies a 3-tier soft boost:MEMO_RECALL_PROJECT_BOOST(0.25, current project) >MEMO_RECALL_GLOBAL_BOOST(0.10, no project tag orpreference/feedback) > other projects (+0).memo migrate --bucket-by-projectre-buckets an existing flat install (non-destructive, idempotent, then reindexes). Backup/restore + git sync userglob, so they preserve the bucket layout.
Test isolation (see tests/conftest.py)
- Use the
tmp_cfgfixture or build an isolatedConfig— never callConfig.from_env()without controlling the environment. CliRunnerinvocations setMEMO_NONINTERACTIVE=1,MEMO_DATA_DIR, andMEMO_STATE_DIRinenv=(conftest defaultsMEMO_NONINTERACTIVE=1).- Real MLX forward passes are gated by
@pytest.mark.requires_mlx(auto-skipped whenmlx_lmisn't importable). - Never read or write the developer's real vault.
- Changing hooks, daemon lifecycle, install/runtime plumbing, or migration
paths: run the broad suite plus the focused modules —
tests/test_recall_hooks.py,tests/test_recall_server.py,tests/test_runtime_isolation.py.
Config & errors
MEMO_*behavioral flags live insrc/memo/flags.py(registry + typed accessors). Storage/model config lives insrc/memo/config.py(typedConfigdataclass). Preferflag_bool/int/float/strover rawos.environ.memo config validatecatches typos.- User-facing config is the persistent Markdown config (
src/memo/config_md.py):memo config/memo config set <key> <value>(dotted keys, e.g.recall.top_k) writes~/.config/memo/*-config.md— reaches daemons/MCP/hooks, unlike a per-terminalexport MEMO_*. Flag resolution: env var > markdown config > tuned overlay > built-in default. - Domain errors live in
src/memo/errors.py(MemoErrorbase). Raise/catch those rather than bareExceptionin non-defensive code.
Releasing
Bump the version in sync across twelve versioned files plus CHANGELOG.md:
pyproject.toml [project].version, .claude-plugin/plugin.json,
plugins/memo/.codex-plugin/plugin.json, server.json (version + package
version), and CHANGELOG.md (Keep-a-Changelog) — memo release bump edits
all five. Commit / tag / push stays manual.
memo release check is the gate. It validates version parity across every
versioned surface (both plugin manifests, server.json incl. extra packages,
the mcpb manifests + archive, install pins, the Homebrew formula, the CHANGELOG
section) and — via src/memo/adapter_matrix.py — three surfaces that carry
no version and whose drift is silent:
| check | catches |
hook-commands-resolve | hooks/hooks.json firing a memo subcommand the CLI no longer registers. Hooks are soft-fail by design, so a rename stops recall/capture/sync with no error anywhere |
embedder-dims-parity | an .mcp.json pinning MEMO_EMBEDDER_MODEL whose MEMO_EMBEDDER_DIMS doesn't match the model size (MLX invariant 3 — corrupts the vec0 table on first write). A config pinning neither stays legal: the installed index is self-describing |
referenced-paths-exist | the codex manifest's mcpServers path or the marketplace source pointing at nothing |
tests/test_adapter_matrix.py mutates one surface per test and asserts the
check flips to fail — a gate that only ever passes proves nothing. Version
parity deliberately lives only in cli_release; a second, weaker opinion that
can disagree with the real gate is worse than none.
Source of truth — role & contract
memo is the canonical store of durable semantic knowledge: decisions,
facts, preferences, learnings. It is consulted automatically (recall hook every
prompt; El Briefing at SessionStart) and stays fresh on its own (memo
maintain supersedes contradictions, merges duplicates, archives stale —
reversibly). The injected recall block is labelled authoritative, so treat
surfaced memories as established facts: prefer them over assumptions, build on
them, and contradict one only explicitly.
Memo is the source of truth for both durable knowledge and the operational continuity needed by agents. Durable facts, decisions, preferences, focus, handoffs, attention, conflicts, and outcome feedback all live behind Memo-owned CLI/MCP contracts.
Contract for any agent using memo:
- Read first — consult memo recall before deciding;
memo_unified_briefingis the entry point. - Write back — persist durable facts to memo via
memo_saveso the next session inherits them. - Respect freshness — memo's contradiction/freshness state is authoritative for durable knowledge; don't reintroduce a fact memo has superseded.
- Identify yourself — every read path is attributable, so
memo usefulnessproves who actually reads memo (a layer that never appears is a silent gap):- MCP tools — pass
source="<layer>"onmemo_search/memo_ask/memo_chat_ask/memo_unified_briefing. If omitted,log_consultfalls back toMEMO_SOURCEthen to the MCP client's handshakeclientInfo.name— so agent clients (devin/opencode/devin-desktop) attribute automatically without per-call args. - CLI —
memo search/ask/chat-ask/recalltake--sourceor readMEMO_SOURCE; a consult logs only when one is set (an interactivememo searchstays out of the stats). - Warm socket — the recall daemon's
{"op":"search","client":"<layer>"}gives sub-second structured recall (no cold CLI) and attributes the client.
- MCP tools — pass
memo deliberately keeps cognition OFF its MCP surface
(test_brain_like_mcp_tools_are_not_registered): no suggest/agent/
cognitive verbs. Proactivity lives in memo's own recall/briefing output (the
"Also in your memory" nudge), not as a brain tool — memo is the store, the
layer above is the cognition.
CI gates
pytest, mypy, ruff (on src/ and tests/), and coverage run per
commit. Keep the suite green.
The runtime-independence job proves the installed distribution does not
resolve retired sibling modules or private contract packages. The definitive
benchmark also gates journal integrity and native event throughput.
Retrieval-regression discipline (every failed search → a system change, measured)
Rule: when a search returns wrong results, do NOT patch that one query. Make a systemic change (ingest quality, ranking, a general gate) and prove it holds across the whole regression set — never per-question.
- Growing committed corpus:
eval/regression_labels.json(schemamemo.eval_recall.labels.v1). Every incident adds a labeled prompt:relevant=true+expect_ids(the note that MUST surface, ≥8 hex prefix), andnoise_tags/noise_path_fragmentsfor records that must NOT crowd top-K (garbled OCR screenshots, archived/old notes). - Gate (fast, no MLX — retrieval only, ~0.5s/prompt):
memo eval recall --labels eval/regression_labels.json --k 5 --force. A retrieval/ingest change must keep precision@K high and noise@K low across ALL prompts (the label set grows with every incident — 46 prompts as of 2026-08; the authoritative numbers are the per-machine saved baseline below, not any figure hard-coded here). Runs against the live index (machine-local, not GitHub CI). - Enforced gate (machine-local, opt-in): seed once with
memo eval recall --labels eval/regression_labels.json --k 5 --update-baseline, then--gate(instead of printing) exits non-zero if precision dropped or noise rose vs the saved baseline — wire it into a pre-commit hook. The baseline lives understate_dir/eval/(per-machine; it tracks your corpus). - Split of concerns: retrieval-class regressions (right note buried, garbage
crowding) gate here; end-to-end grounding, abstention, and integrity gate in
memo definitive checkplus the relevant Memo eval command.
Behavior eval — did the recalled memory actually steer the answer?
memo eval recall stops at "the right note surfaced". A memory can be top-1 and
still be ignored, re-litigated, or answered around, with every retrieval metric
green. memo eval behavior (src/memo/eval_behavior.py, corpus
eval/behavior_scenarios.json, schema memo.eval_behavior.scenario.v1) covers
the step after retrieval:
- seeds an isolated store with the scenario's memories (real store, real embeddings — nothing mocked),
- runs the real
memo recall-hookas a subprocess against it and takes itsadditionalContextverbatim (no ranking is reimplemented, so the harness cannot drift from the hook), - feeds
[injected block + prompt]to a model and scores the scenario's gates.
Gates are two-layered: must_recall / must_not_recall (deterministic, from
the block) and answer_must_contain_any / answer_must_not_contain_any /
semantic (from the answer). A scenario with only recall-layer gates is
rejected at load — that is retrieval, already covered above.
memo eval behavior --recall-only # deterministic, no LLM loaded
memo eval behavior --gate # full, exits non-zero on any failure
memo eval behavior --only <scenario-id> --jsonRead a red gate correctly. The model in step 3 is memo's own local LLM, not
Claude — a failure proves the injected payload under-steers a competent model
(block formatting, labelling, ordering, truncation, token budget: all memo-side),
never that a specific agent misbehaved. The answerer and judge both default to
MEMO_HELPER_MODEL, never the 30B generation model: the embedder is already
resident during a run, and stacking a 30B answerer + 30B judge on top is the
residency mix that has OOM'd this machine before.
A corollary of the small answerer: a red answer-gate can also mean the
answerer was not competent enough to quote a specific. A failing
answer_must_contain_any therefore reports whether the demanded fact reached
the block at all — absent is memo-side and unambiguous (the payload cannot
steer toward a fact it does not carry); present is real under-steering or
a weak answerer, not separable without a stronger one. So --recall-only is
the trustworthy gate today and full mode's absolute score is diagnostic, not a
pass/fail line. First live corpus run: 7/8 recall-only, 18/38 gates full.
Open findings from the first live run (superseded-fact-must-not-drive-the-answer):
- With two conflicting memories in the block, the model answered from the wrong one and cited it. The block is a flat list with no freshness or supersession signal in the rendering, so nothing marks which is current.
- Body-bullet truncation surfaced a misleading fragment: the first visible line
of the memory whose entire point is "the chat port is 8767" was truncated to
En esta máquina el 8765 lo ocupa ...— showing only the wrong port. This one is independent of supersession and reproducible.
Not yet fixed: both are ranking/rendering changes and must go through
memo eval recall --gate (the pre-push discipline above), not a quick patch.
Dream — nightly self-maintenance + self-improvement
memo dream run is the nightly pipeline (LaunchAgent com.memo.dream at 03:00;
memo dream if-due for the >24h guard). It is separate from memo maintain.
Wiring is src/memo/cli_dream.py (Click) + src/memo/cli_dream_passes.py
(_run_* implementations); the receipt persists to state_dir/dream/last.json
(memo dream status). Every pass records its result there and failures land in
receipt["errors"] — never silently swallowed.
- Maintenance passes (default-on): orientation inventory → signal-gather
(transcript mining → memories) → contradictions (supersede ≥0.9) → consolidate
duplicates → archive stale →
synthesize_cross_cluster→ entities → ROI decay → prune-floor / LFU-evict → compress → prewarm. - Dream v2 — self-improvement passes (opt-in per flag; all default OFF
EXCEPT
MEMO_DREAM_FLAG_GRADUATION_ENABLED, which has defaulted ON since v4.3.0):- Tuner (
dream_tune.py,MEMO_DREAM_TUNE_ENABLED): mines ground-truth-by-use labels fromgrounding.log(reuseseval_recall.harvest_labels), line-searchesMEMO_RECALL_MIN_SIMover the live index, and auto-applies the winner — gated by the curatedeval/regression_labels.json(a change that helps mined labels but hurts the curated set is rejected) and auto-reverted when a later night regresses vs the saved baseline. CLImemo dream tune. Scope: it only movesmin_sim— the one knob the tuner line-searches today. The blocker this used to name is gone:rank_hits()is already public and pure (recall_logic.py:1388) and is the shared ranking path for the recall hook, the eval harness and the A/B seam, so boosts and rerank-pool are measurable now withmemo eval recall. Widening the tuner to them is unstarted work, not a missing extraction. - Consolidate (
dream_consolidate.py,MEMO_DREAM_CONSOLIDATE_EPISODES_ENABLED): groups recent episodes (EpisodeStore.recent) by project and abstracts recurring cross-session work into one durabletype=synthesismemory (synthesis_kind=cross_session, session ids as provenance, dedup by provenance hash). Distinct from per-turn signal-gather and cross-memory synthesize; no destructive episodic decay. - Anticipate (
dream_anticipate.py,MEMO_DREAM_ANTICIPATE_ENABLED): surfaces recurring unmet gaps (outcome.detect_gaps) + hot queries into the receipt/briefing and pre-warms their embeddings. Never fabricates. - Flag graduation (
dream_flags.py,MEMO_DREAM_FLAG_GRADUATION_ENABLED): every default-off*_ENABLEDflag declares a gate indream_flags.GATES(recallA/B via the evalflag_overridesseam /tuner-owned /manual- reason) — completeness CI-enforced by
test_dream_flags.py, so a new dark flag cannot merge without declaring its gate. Winners graduate to ON via the overlay afterMEMO_FLAG_GRADUATION_WIN_NIGHTSconsecutive wins (latency + curated gates), auto-revert on regression; flags un-graduated pastMEMO_FLAG_GRADUATION_DEADLINE_DAYSbecome cull candidates inmemo dream graduate-flags --status(deletion stays human). Distinct fromdream_graduate.py(quarantined-memory graduation).
- reason) — completeness CI-enforced by
- Tuner (
Tuned-params overlay (src/memo/tuned_overlay.py) is the only place
auto-tuning touches live behavior: the tuner writes state_dir/tuned_params.json
and flags.flag() consults it, so flag resolution is env var > markdown
config > overlay > built-in default (an explicit MEMO_* env var or
memo config set value is never overridden). Delete the
file or memo dream tune --rollback to revert. The enable flags live in the
nightly LaunchAgent's EnvironmentVariables (launchd does not inherit the shell)
— see ~/repos/memo/launchd/com.memo.dream.plist.
Codegraph-first (code intelligence)
The repo is indexed by codegraph (.codegraph/codegraph.db, kept fresh by the
MCP watcher + git hooks + nightly sync). Use it INSTEAD of the grep+Read loop:
- Understand a flow / architecture / bug: ONE
codegraph_explorecall with 2-4 exact symbol/file names (a bag of names, never prose — retrieval is lexical FTS5, no embeddings). Don't know the name?codegraph_searchfirst. - Treat explore output as Read-equivalent: don't re-verify with grep, don't delegate to file-reading subagents.
- Before
codegraph_callers/codegraph_impact: confirm the symbol EXISTS withcodegraph_search— a non-existent name silently substitutes the best fuzzy match (upstream #1473). - After editing: a ⚠️ staleness banner means re-read with Read; in doubt,
codegraph_statusand checkpendingChanges. - Do NOT trust the "⚠️ no covering tests" blast-radius note (~40% false,
upstream #1475) — verify against
tests/with grep. - Overloaded names (
search,save,recall): pin with file/line viacodegraph_node. - Cross-repo (memflow/synapse): pass
projectPath— graphs are per-repo, no cross-repo edges. Caveat: synapse consumes memo via subprocess/CLI, so 0 cross-repo callers does NOT prove a CLI surface is unused. - Pre-refactor gate: run
scripts/cg_impact_gate.sh <symbol>before renaming or changing signatures undersrc/memo/. Success criterion for a rename:codegraph_callerson the OLD name returns 0. codegraph affectedis BROKEN for this repo's src-layout (returns 0 tests) — usescripts/cg_affected_tests.sh(SQL over the DB) instead.
Review with codegraph
Per file in the diff: codegraph_node on the file → its dependents are the
review checklist (each caller appears in the diff or is justified). Per renamed
symbol: codegraph_callers <old-name> must return 0. The CLI's
node --symbols-only output truncates dependents with +N more — when that
marker appears, expand via the codegraph_node MCP tool, never trust the
truncated list as complete.
Workflows (Claude Code dynamic workflows)
Saved orchestration scripts in .claude/workflows/, invoked as /-commands:
/demonolith-split [path]— map a god-file and propose a clean in-repo package split (plan only). No arg → largest source file (e.g.cli_capture.py).
Use /effort ultracode when a change benefits from parallel in-repo
implementation and review.
<!-- memo-mandate -->Memory-first (memo)
Before deciding or answering anything that prior work might already cover, consult memo FIRST:
- Start with
memo_unified_briefing(ormemo_search/memo_ask) to pull durable facts, decisions, and preferences. - Already hold a memory id or a named entity and want what's connected to
it? Reach for
memo_graph(cheap graph traversal — id/title only; the richermemo_relatedexists only on the full/default MCP profile) before firing a freshmemo_search/memo_ask(full retrieval) for the same thing. - Pass
source="<this-client>"on the read tools so usage is attributed (e.g.source="codex"). A client that never appears in memo's consult log is flagged as a silent gap bymemo usefulness. - Write durable outcomes back with
memo_saveso the next session inherits them. memo is the source of truth for what is known; build on it, and contradict a surfaced memory only explicitly. - Keep it honest: when a surfaced memory is stale or contradicted, correct it
instead of silently working around it —
memo_invalidatecloses its validity interval andmemo_supersedepoints it at the replacement. Both are reversible and never hard-delete. (memo_feedback_flagdoes the same thing but is only registered on the full/default MCP profile, not onagent/core.)
<!-- /memo-mandate -->Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

