./run foldcrumbs-memoria-persistente-agenti-grep
foldcrumbs: agent memory with grep, not a vector DB
Technical diary of foldcrumbs: persistent memory for coding agents built on grep instead of a vector DB. Zero dependencies, one stdlib MCP server, and a…
- status
- shipped
- project
- engram
- updated
- 2026-09-18
- tags
Every /clear wipes the agent’s brain. Every session starts from scratch, asks you the same questions, forgets yesterday’s decisions. The obvious solution is a vector database with embeddings. foldcrumbs (born as engram) does the opposite, and the whole thesis is right here: the agent is already a first-rate retrieval engine — don’t build a second one.
The thesis: grep, not vectors
A coding agent (Claude Code, Codex, OpenCode) already has native Grep and Read and knows how to use them. foldcrumbs writes memory as a folder of markdown files and lets the agent grep them the way it greps source code. No embeddings, no index to rebuild on every write, no similarity search, no service to keep running. The technical argument, in full:
- Recall is instantaneous and doesn’t depend on a model being up. The LLM is used only for asynchronous distillation (turning a finished session into memories). Retrieval touches neither model nor network.
- Zero moving parts versus a vector stack (Docker + a retrieval engine + LLM + REST API).
- Grep is readable and debuggable. Memory is markdown you can
cat, diff, sync with Syncthing. No opaque index that silently drifts out of sync with the truth. - Scale is a Phase 3 problem, not a Phase 1 one. Embeddings + an open vector DB arrive only if scale outgrows grep. The bet: per-project memory is small (dozens of one-line facts), and there grep + a curated index beats vector search on latency and correctness.
Recall is two-tiered: an index that’s always injected (deterministic, rides the prompt cache) plus on-demand grep into the folder when detail is needed.
The format
One markdown file per memory, frontmatter + body, written atomically (tempfile + os.replace). Thirteen typed kinds (fact, decision, preference, instruction, error, goal, learning…), a provenance with a confidence weight (explicit_statement=1.0 … inferred=0.7), and a decay model: confidence falls with age only for preferences and observations, a contradiction knocks it down to 30%, a validation promotes it.
---
name: Recall is grep, not a vector DB
description: The agent's own Grep/Read is the retrieval engine; no embeddings.
type: decision
confidence: 0.9
provenance: explicit_statement
tags: architecture, retrieval
---
Memory retrieval is delegated to the agent's native grep over the memory folder.
Embeddings/vector search are deferred to Phase 3 only if scale outgrows grep.
The index that rides the prompt cache
The MEMORY.md file is one line per memory, grouped by type. The load-bearing design detail is determinism: within each type the entries are ordered by immutable created_at, not by mutable title. This way a confidence bump or a re-distillation doesn’t reshuffle the existing lines — only adding or removing a memory changes the file. The payoff: the prefix injected at the start of a session is byte-identical across sessions, so it rides the agent’s prompt cache instead of invalidating it, and the file stays diff-clean for Syncthing.
Recall: the agent is the engine
Two things cross the context boundary. The first is index injection: a SessionStart hook reads MEMORY.md and emits it as additionalContext, with the instruction “honor it; don’t re-ask what’s already recorded; for detail, read the linked file or grep the folder”. The second is the agent’s own on-demand grep. No code path does semantic retrieval at recall time. For the tools that need a programmatic equivalent (the CLI, the recall MCP tool), there’s a stdlib ranker: exact match = 1.0, otherwise word overlap + difflib.SequenceMatcher.
Integration: hooks + one stdlib MCP server
The hooks are agent-agnostic — they read cwd/transcript_path from the payload — so Claude Code and Codex reuse the same scripts under different event names. The installer is merge-safe and idempotent: it appends its own hook groups with a marker, leaves existing ones intact (GSD, graphify…), and backs up first.
The MCP server is written from scratch in stdlib: JSON-RPC 2.0 over stdin/stdout, no dependency on the mcp SDK. It exposes three tools over the shared store — remember, recall, answer.
Anti-context-rot
A monitor on PostToolUse estimates tokens by reading the whole transcript (chars / 3.5). When it crosses 45% of a 200k window, it fires once: it spawns a fully detached distillation worker (stdio to /dev/null, start_new_session=True) so the LLM call never blocks the editor, and injects a reminder: “checkpoint saved in the background, good moment for /compact or /clear, nothing is lost”. A small per-session state avoids re-firing on every tool call.
Why pure stdlib, zero dependencies
pyproject.toml: dependencies = []. It’s the central constraint, and it cascades into explaining everything:
The hooks run on the developer’s hot path, on every tool call, launched by the editor. A missing or incompatible import (pydantic, the MCP SDK, an embedding library) would break Claude Code itself. Zero dependencies = the script always imports.
Every hook runs under a “never break Claude” contract: any exception exits with code 0, silently. The concrete substitutions: dataclasses instead of pydantic, difflib for fuzzy matching, urllib for the LLM call, a hand-written JSON-RPC MCP instead of the SDK. Local distillation can run entirely on MLX or Ollama, so a shared store can have a single machine as the indexer while the others stay read-only consumers.
How it’s going
Shipped, v0.3.0 on PyPI, MIT, CI green. File store + index, grep recall, hooks for three agents, anti-rot monitor, four distillation backends, the stdlib MCP server, secret redaction. Phase 3 (embeddings, only if grep is no longer enough) is future — and the fact that it’s still hypothetical is, to me, the confirmation that the bet holds.
Updates
2026-09From a notes folder to a ledger: a graph, a fleet, and a from that shut everything down
Since July foldcrumbs has gone from 0.3 to 0.11 and stopped being just “greppable markdown”: there is now a graph layer (derived relations, explicit relations, and model-proposed relations that go through an approval queue), document ingestion with tracked provenance, and an MCP surface grown from three tools to eleven. The two additions I’m happiest about are invalidation contracts — invalidated_by declares “this fact holds as long as that one does”, and when the base fact dies the dependent drops out of the served context, derived on every read, with no daemons and no cascades — and the authorization type: permissions are no longer prose, they are a typed register with a mandatory expiry and a live backing, mintable only from the CLI, never by an agent. On recall I replaced the best-of between the lexical and semantic channels with a rank fusion (RRF): with the semantic flag off the ordering stays byte-identical to before, so nobody pays for an optional feature. I also added explicit adoption of one memory at a time across federated stores, attested in a local register instead of in the frontmatter — because a hand-written source: adopted: line must not attest to anything. The most instructive bug of the quarter was trivial and devastating: the graph_path tool declared a parameter named from, which is a reserved word in Python; an MCP host that builds one function per tool from the schema blew up while mounting the list, and the agent lost all of its tools while foldcrumbs looked perfectly healthy — the parameters are now called source/target, the old names stay as undeclared aliases, and a suite test rejects any schema whose names are not valid Python identifiers. That’s fine: the original bet (grep instead of a vector DB) still holds, but nearly all the recent work went into what memory has the right to say, not into how to find it.
Code: github.com/vcnngr/foldcrumbs · pip install foldcrumbs.


