Memory¶
Memory is angelo's persistent knowledge across sessions: the place an agent
records why it did something, what it tried, and what it learned — so the next
session (yours or another agent's) starts informed instead of blank. It is one
file-backed store with several faculties, all backed by markdown in .memory/
and committed to git.

The same node set, re-projected: a structural tree of parent → child lineage becomes semantic clusters that pull cross-branch entries together by meaning.
- Package overview:
memory/README.md - Behavioral contract:
.cursor/rules/memory.mdc - Storage/commit contract: LLM agent guide
- Tool reference: MCP tools → memory
Four faculties over one store¶
Everything lives as markdown + YAML under .memory/; the KGLite cache in
.angelo/ is disposable and rebuilt on demand. On top of that one store, memory
offers four faculties:
- The research tree — a typed hierarchy of decisions, plans, experiments, and checkpoints that records why the work went the way it did.
- The skills library — reusable procedures and patterns an agent can save once and recall later.
- The code graph — a live structural map of the repo's functions, classes, and modules, queried instead of grepping.
- Session continuity — handoff/pickup so a new session inherits the last one's focused context.
Because all four are files, they travel with the repo, diff in review, and survive process death — the memory is durable, not a cache that a crash loses.
The research tree¶
The tree uses a fixed four-level structure under a single root project:
Entries are typed — plan, checkpoint, experiment, decision, todo, note, annotation — and placed under the most specific entry they relate to, so provenance is encoded in the hierarchy itself. There is exactly one project per repo; new workstreams are subprojects, not new projects.
The decisions recorded here are mined into the browsable architecture decision records.

The Tree tab's Plan view collapses to just the plan-node scaffold (subprojects → phases) — the backbone the typed entries hang from.
Two views of the same tree¶
The dashboard renders that one node set two ways — a structural tree of parent → child lineage, and semantic clusters that group entries by meaning regardless of where they were filed. They answer different questions and neither replaces the other. See Tree and clusters.
How search ranks results¶
search blends three recall channels — file-path match, keyword (title beats
tags beats body), and semantic (model2vec vector similarity) — and fuses the
keyword and semantic rankings with Reciprocal Rank Fusion. That fused order is
the default result order.
On top of that recall, an optional blended reranker (shared with the
zettelkasten) can reorder the hybrid-mode results by folding in two more
signals and a diversity pass:
- Importance — an entry's degree centrality in the tree (
has_child/related/ supersession edges). Hubs — heavily-linked decisions and phase roots — get a boost. - Recency — a half-life decay over each entry's
updated_at/created_at, plus a lift for entries touched in the current session, so work-in-progress surfaces first. - MMR — Maximal Marginal Relevance diversification, which reduces near-duplicate clustering in the top results.
The reranker is non-punitive and off by default: with its weights at zero it
reproduces the plain hybrid-RRF order exactly, and a signal a candidate lacks
(an isolated leaf, a never-touched entry) simply doesn't contribute — it is never
pushed down. Turn it on and tune it with environment variables:
MEMORY_RERANK_W_IMPORTANCE, MEMORY_RERANK_W_RECENCY,
MEMORY_RERANK_MMR_LAMBDA (1.0 = no MMR), MEMORY_RERANK_OVERFETCH, and
MEMORY_RECENCY_HALF_LIFE_DAYS. When on, each result carries a rerank
breakdown (fused/importance/recency) so you can see why it ranked where it
did.
Navigating a big tree: briefs and completeness¶
With well over a thousand entries, the tree needs orientation aids beyond the
raw hierarchy. recall provides two, both additive — they never change the
default tree output:
- Completeness score. Every plan node (subproject / phase) carries a
completenessscore in[0, 1]— a deterministic, transparent read on "how done is this?".
A leaf phase (a plan node whose children include no plan nodes — the same
definition open_phases uses) is scored directly, blending three signals
already on the node: a concluding checkpoint (the strongest done-signal),
the resolved-thread ratio, and type balance (substantive
decisions/experiments/checkpoints vs. only open todos). Crucially the
done-signals (checkpoint present, substantive balance) are read from the
active children only, and a phase with no active children left (everything
discarded) scores the floor 0.0 — an abandoned/retracted phase must never
score higher than a genuinely concluded one. The resolved-thread ratio counts
only threads resolved by a successor (superseded via invalidated_by), not
bare discards: a plain discard is abandonment/churn, not completion, so
discard-churn can never inflate the score above an in-progress or cleanly
concluded phase (a messy phase with one active checkpoint and nine abandoned
children must not outrank a clean [decision, checkpoint] phase).
A container plan node (a subproject, or the project root — any plan node
whose children are themselves plan phases) does not use the direct formula
(its children are phases, not leaf work, so it would spuriously read ~0.0).
Instead its completeness is a rollup = the mean of its descendant leaf-phase
completeness scores (descending only through active plan children). So a
subproject of one 0.7 phase reports ~0.7; a subproject of a 0.7 phase and a 0.1
phase reports ~0.4; a container with no active leaf phases reports the 0.0
floor. This keeps the promise that every plan node carries a meaningful
"how done is this?" all the way up the tree.
This score is an intentional additive field: it appears on every plan node
in the open_phases list and in the tree / outline payloads (nothing
existing is reordered or removed), so a consumer that snapshots plan-node shape
will see the extra completeness key. The weights are deliberately simple and
tunable (see _COMPLETENESS_W_* in memory/server.py) because the formula is
a judgment call, not ground truth.
- Briefs (
recall(view="brief", entry_id=…)). A brief is a rollup summary of a node's subtree: its child titles/types/statuses, the concluding checkpoint (if any), the descendant type histogram, the open-thread count, the most-recent activity date, the completeness score, and the top-N most central entries in the subtree (reusing search's importance channel, with a recency fallback). It is deterministic-first — no LLM required — so it builds and tests offline.
The concluding checkpoint is active-only: even under
include_discarded=true (where the brief's children list carries discarded
rows), a retracted checkpoint is never named as concluding_checkpoint, so the
brief agrees with completeness (which likewise credits only active
checkpoints) on the same node.
A brief is cached off-file in the long-lived MCP process' memory
(memory.server._brief_cache, an LRU-bounded OrderedDict), keyed by
(project, entry_id) and validated by a descendant signature
(descendant_count + the max updated_at over the subtree including the node
itself). The project component of the key prevents a wrong-project brief on an
entry-id collision (ids are only unique within a store); the LRU cap
(_BRIEF_CACHE_MAX) bounds growth so discarded/moved nodes can't retain briefs
forever; and _rebuild_runtime_cache clears the cache on every reload/project
switch. Deliberately not persisted to the entry's committed frontmatter:
recall(view="brief") is a read and must stay side-effect-free w.r.t. git —
writing a cache into a tracked file on every read would dirty the working tree
(perturbing record's dirty-tree pin classifier) and fail on a read-only
replica. The cache is disposable like the .angelo/ runtime caches (e.g.
memory.kgl); losing it just triggers a cheap recompute. Re-summarizing an
unchanged subtree is recompute-free (the response is flagged cached: true);
adding, removing, or touching a descendant (or the node) changes the signature
and invalidates the cache automatically. This mirrors the reranker's
graph-signature caching. (Known limitations: an edit that does not bump a
descendant's updated_at can leave a stale brief; and top_central is derived
from a global importance normalization, so a link() in a sibling subtree
can shift those scores without touching any timestamp in this subtree — the
ordering of already-listed central entries is therefore best-effort and may lag
until the next in-subtree change. The subtree-local headline signals
(children/checkpoint/completeness) are always accurate.)
An optional prose layer turns the deterministic rollup into a 2–3 sentence
narrative. It is strictly opt-in (prose=true) and only fires when a narrative
generator is injected (memory.server._brief_summarize_fn, default None) —
the memory package never imports an LLM or zettelkasten to produce one. The
prose is derived solely from the deterministic brief, so it cannot invent
structure.
When to read vs. write¶
- Read first. Before exploring or grepping the codebase for a question or
task, call
search/recall/session pickup— prior work, decisions, or the answer often already live in the tree. - Write deliberately.
recordfor significant decisions, experiment outcomes, and checkpoints — and, mandatorily, before every substantive git commit, so each code change travels with a memory entry pinned to its SHA.
Git-pinned references make this reproducible: pass a files list when recording
and the server commits those files and pins the entry to the resulting SHA, so
git show can always reconstruct exactly the code the entry describes.
Code graph¶
Alongside the research tree, the server keeps a live code graph — a
structural map of the repo's functions, classes, and modules that agents query
with the codebase() tool instead of grepping or reading files. It's built by
parsing the working tree into a KGLite graph, cached in-process, and
auto-rebuilt whenever git HEAD changes, so it always reflects the current
checkout without a manual refresh.

The code has its own graph (right), queried via codebase(). Dotted tethers show research-tree entries git-pinned into modules — every entry pins a commit SHA, so git show can reconstruct exactly the code it describes.
codebase(action=..., ...) is an action dispatcher over that graph. Each
action returns a decision-shaped payload — ranked, capped, with file:line and a
next hint naming the follow-on call — rather than a raw graph dump:
lookup(the default, back-compatible) — name/scope symbol search. Barecodebase()returns a census of the graph (how many functions, classes, modules),codebase(query="build_agent_registry")matches nodes by name, andscope="coordinator/"restricts to a directory subtree.blast— "what breaks if I change this?". A reverseCALLS/IMPORTSwalk from a symbol or file, returningimpactedentries (each withqualified_name,file,line,distance, and aseverityofbreaksormaybe) plustests_affected.callers/callees/imports— tighter 1-hop views of the call and import neighborhood.context— "why does this exist?". For a symbol or file anchor it returns a rolling tip and a provenance chain (below);expand_synapse=truehops from the tip to related zettelkasten canon.health— graph-metric health with located anti-pattern drivers (god objects, coupling, cycles, churn hotspots, dead code).ownership— "who wrote this / who do I ask?". Resolves the anchor, git-blames the symbol's line range (or the whole file, forpath=), and aggregates by git author into a rankedownerslist — each with a line count, commit count, and the author's last commit (sha + date). Thememory-mcpbot author is excluded so the owners reflect human contributors. It degrades gracefully (an informative note, never a raised error) when there is no git repo, blame fails, or the anchor is absent.
The tether: tip + chain. The memory behind a symbol is derived from the file
pins you already make — you never hand-maintain it. When record(..., files=...)
pins a file, the server diffs the pin commit against its parent, intersects the
changed lines with the graph's symbol ranges, and attaches the entry to the touched
qualified_names (tagged changed) or, when the diff is empty or the file is
unmappable, to the file anchor (tagged referenced). Each symbol then exposes an
append-only chain (every entry that touched it, oldest to newest) and a rolling
tip (the newest high-signal entry — decision / experiment / checkpoint —
preferring changed over referenced). The changed vs referenced split is
computed against the symbol's span at the pin commit (a Python reparse of that
revision), so a rename or line-shift no longer mislabels a genuine edit as merely
referenced; non-Python languages fall back to the live span. The tip advances only
when a new pin lands, never silently from git. This makes the "dotted tethers" above
real at the API layer: context is how an agent reads why a symbol is the way it
is before changing it.
Julia (.jl) and other languages the graph doesn't parse degrade to file-level
anchors — the chain and tip still work at file grain, without Class.method
symbols. Anchor resolution now also handles a bare name in an unparsed language:
a name query with no parsed symbol resolves to a file anchor when exactly one source
file defines that name (path queries already worked). When several files define the
name it stays ambiguous rather than guessing.
Together this is complementary to search and recall, which navigate the semantic
tree of decisions and plans rather than code structure.
Skills and sessions¶
- Skills are reusable procedures saved with
skill(action='save')and found later withskill(action='search')— the library an agent builds of "how we do X here". Browse and prune it from the dashboard's Skills tab. - Sessions persist what a chat did:
session(action='handoff')finalizes the work, andsession(action='pickup')loads the previous session's focused context (or searches past sessions by topic) so a new chat resumes with intent.
Seeing it: the dashboard¶
The memory tree is excellent for agents and git but a poor thing for a human to read as a directory of frontmatter files. The memory dashboard projects the same files into scannable views — the tree and clusters, the skills library, live and past sessions, experiment provenance, and in-flight coordinator runs — without ever becoming a second source of truth. See Dashboard and the how-to, Use the memory dashboard.
Experiments and artifacts¶
A toggleable companion server, memory-artifacts, captures experiment provenance (code SHA, data hashes, command, environment) and backs large or regenerable files with DVC/S3 while git keeps only lightweight pointers. It rides on the same tree — experiment entries link to their manifests — and can be enabled or disabled independently of core memory. See Experiments and cloud reruns.
Commands¶
Natural-language phrasings that trigger memory — say the thing, the agent runs it. See the full command cheatsheet.
| Say this | What runs |
|---|---|
| "commit", "commit this", "push it up", "commit and push" | Record a memory entry then git commit — the code change and its entry travel together |
| "what were we doing", "catch me up", "pick up where we left off" | session(action="pickup") |
| "wrap up", "hand off", "save the session" | session(action="handoff") |
| "log this decision", "checkpoint this", "record that" | record(...) |
| "remind me to…", "add a todo", "we should do X later" | record(type="todo") under the most specific entry |
| "show the research tree", "what's the plan", "recall" | recall |
| "have we tried this", "search memory for X" | search |
| "where is X defined", "what lives in this module" | codebase() |
| "save this as a skill", "remember how to do this" | skill(action="save") |
| "open the memory dashboard" | launch_dashboard |
Related¶
Semantically related entries from the memory graph.