Ultan — Persistent Memory for Claude Code
Ultan is a neuroscience-inspired, agentic memory system for Claude Code — a layered architecture that learns your preferences and coding conventions and recalls them across every session, project, and machine. A pair of curator agents tends the library: the Librarian keeps your markdown knowledge graph neat — deduped, linked, and organised — while the Scholar gates what's worth keeping and keeps every entry accurate and up to date as things change. On retrieval, a stack of BM25, vector embeddings, cross-encoder reranking, and agentic search surfaces the right memory at the right moment. Plain markdown on your disk you can grep and git. No cloud.
⚠️ Beta. Under active development. Expect breaking changes and rough edges — feedback welcome.
"Show me a man who has read all of the books of one of the major branches of knowledge — say, military history — and I'll show you a man more ignorant than the merest churl. For while he has read, others have written; and the body of available knowledge has grown so much faster than his understanding of it that he is, on balance, less learned at the end of his studies than at their beginning."
— Master Ultan, Gene Wolfe
Why this exists
The memory features that exist today — CLAUDE.md, Cursor rules, ChatGPT memory, the various provider built-ins — are there but I always felt they did not surface the lessons that had been learned well enough. They seem to be rarely useful, bloat the context, get ignored and when they fire they often feel trivial: shallow grep against a static rules file, no judgment about what's worth remembering vs what isn't, no idea what's stale, no composition with the current turn. I was tired of teaching the same agent the same thing over and over and I did not want to have to manually curate an ever changing set of shared knowledge per project and globally.
So I wondered what could be achieved with a much more advanced system. There are obviously many alternatives but none had all the features that I wanted. Real memory is salience-gated at write time, decays without reinforcement, mutates on retrieval, resists deletion of high-importance traces, and uses different mechanisms for different latencies (ambient familiarity, deliberate recall, fast suppression). So we took the neurology seriously and built towards it — a curator pair (currently Sonnet + Opus) gating writes by surprise magnitude; three retrieval tiers each tuned to a different cognitive analog; surfacing-aware decay with optional arousal pinning; an opt-in mutation/reconsolidation pathway; archive-don't-delete so contradictions can resurrect old traces. Tokens cost something — it's deliberately token-heavy, and the curator runs on your Claude Code subscription (Max/Pro quota, not a metered API bill) — but less than the friction of repeating yourself.
Ultan watches your conversations as you work, learns your preferences and conventions, and surfaces them when they matter. It's the "remember when you told me to always use uv" that you wish Claude already did natively, except organised, deduplicated, validated, and proactively consulted before the agent interrupts you to ask something you've already answered.
It's your library. On your disk. In plain markdown. You can ls it, cat it, git it.
Quick start
Ultan installs as a native Claude Code plugin — no editing settings.json, no
daemon to babysit. You just need uv on your PATH; the
plugin uses it to provision Ultan's runtime on first use.
Inside Claude Code:
/plugin marketplace add nickroci/ultan
/plugin install ultan@ultan # choose "user" scope to enable it in every project
/reload-plugins # load the hooks + MCP into the running session
⏳
/plugin installlooks frozen — it isn't. Claude Code shows no progress while it downloads the plugin, which can take a minute. Don't cancel; the "plugin changed" confirmation appears when it's done. After that, the first session provisions Ultan's retrieval stack (torch + the embedding/rerank models — a few hundred MB) in the background, and you can keep working while it finishes.
That's it. Skills and slash commands hot-load the instant you install; /reload-plugins
pulls in the hooks and the MCP server. A full Claude Code restart is not required —
your next prompt kicks off provisioning automatically (a fresh session works too; you
don't need one).
🩺 Wondering what it's doing? Ask Claude to run
ultan doctor. It reports whether the background install is still running, the daemon's state (warming / healthy / idle), priming latency, and capture freshness — at any stage, even mid-install.
The plugin provisions that retrieval stack into its private storage in the
background, triggered by whichever happens first: the plugin's MCP server starting
(right after install/reload — the spec has no install-time script, so this is the
earliest the plugin gets to run), a fresh session's SessionStart, or your next
prompt. All three funnel into one lock-guarded installer, so they never race. Until
it finishes, priming falls back to a fast lexical scan; after that the daemon
lazy-starts on demand. Models download anonymously from HuggingFace — see
First-start expectations below.
You now have:
/ultan <text>to save a memory,/ultan-advisor <question>to consult the library- the
ultan-searchskill and theultan_recallMCP tool - automatic priming on every prompt
- everything under
~/.agent-mem/knowledge/as plain markdown — local, no cloud, no telemetry
Where your memories live
Everything Ultan owns lives under ~/.agent-mem/ on your local disk — no cloud sync, no hosted database, no telemetry. Override with AGENT_MEM_HOME=/some/other/path if you want a different root.
| Path | What's in it |
|---|---|
~/.agent-mem/knowledge/ |
Your library — plain markdown. This is the data; everything else in this table is derived or transient. ls, cat, git init it. |
~/.agent-mem/events.jsonl |
Append-only stream of hook events. Hooks write, the daemon tails. Truncates on rotation. |
~/.agent-mem/daemon.log |
Rotated daemon log (~5 MiB cap). |
~/.agent-mem/.bm25.idx, .embeddings.idx |
Search indexes over the library. Rebuilt automatically when the library changes. |
~/.agent-mem/sweep-state.json |
Last-decay-sweep timestamp (24h cooldown). |
~/.agent-mem/pending-nudges.md |
Scholar writes nudges here; the hook reads and clears them on the next turn. |
~/.agent-mem/cost.json |
Running tally of LLM spend across Librarian / Scholar / Advisor calls. |
~/.agent-mem/runs/ |
Per-call audit log (cost, duration, decisions) + full LLM transcripts (7-day TTL). |
See Storage on disk below for the full layout including the folders inside knowledge/.
First-start expectations (model downloads)
The Tier-1 retrieval pipeline uses two open HuggingFace models, downloaded
into your local cache on first daemon start (~/.cache/huggingface/,
re-used by every subsequent run):
| Model | Role | Size on disk |
|---|---|---|
nomic-ai/nomic-embed-text-v1.5 |
sentence-transformer embedder (asymmetric query/document, Matryoshka 768→128) | ~270 MB |
cross-encoder/ms-marco-MiniLM-L-12-v2 |
reranker that scores (query, body) pairs after RRF fusion | ~130 MB |
Both are downloaded anonymously — no HuggingFace account or token is
needed. If you have outbound network restrictions, pre-cache them on a
machine with internet access and copy ~/.cache/huggingface/hub/
across. nomic-embed-v1.5 requires trust_remote_code=True because its
RoPE attention module ships as custom code in the model repo; the daemon
sets this only for the nomic model name, not as a global default.
Boot time on a warm M-series Mac (cold models, warm HF cache):
- ~1 s for the BM25 index
- ~17 s for the embedding model to load + index the library + JIT-warm the query-side MPS kernels
- ~5 s for the cross-encoder to load + run a full-pipeline warmup query
Total: ~25 s from uv run agent-mem-daemon -v to "priming RPC ready".
The startup runs a single end-to-end priming search before opening the
socket — by the time the hook can connect, every MPS kernel in the
retrieval path is JIT-compiled, so the first real request is hot.
Steady-state resident footprint: ~2.5 GB physical memory on Apple Silicon (unified memory, includes Metal device-side allocations); peak of ~3.4 GB during boot warmup. On a discrete-GPU box the split between RSS and GPU memory looks different; on CPU-only hardware the daemon still works but per-request rerank latency drifts up.
When the daemon is running, the UserPromptSubmit hook makes a Unix-socket
call into it (hard cap 2 s; warm steady-state ~300-500 ms) to get a
priming snippet keyed on your current prompt (not the last batch's
curation). When the daemon is down, the hook falls back to a tiny
in-process lexical scan so you still see relevant entries.
To save a memory explicitly: /ultan never deploy to prod without my explicit OK.
To ask before asking the user: /ultan-advisor should I use respx or hand-roll an httpx mock?.
Design, in one paragraph
Ultan is modelled — deliberately, at the level of the architecture, not as decoration — on how mammalian brains decide what to remember and how to surface it again later. Three ideas drive the whole system:
- Surprise gates storage. Memory is not a transcript. The brain encodes events that violate prediction; novelty and reward-prediction-error are the dopaminergic signals that license hippocampal write (Lisman & Grace, 2005; Schultz, Dayan & Montague, 1997). Ultan's curator does the same: it captures an entry only when the user has told it something a competent assistant would not have produced unprompted. No surprise, no write. The three salience signals — contradicts, novel, reinforces — are direct analogues of prediction-error, novelty, and reactivation/consolidation (Sinclair & Barense, 2019).
- Two systems, asymmetric bars. Fast recall-tuned detection, slow precision-tuned deliberation — System 1 and System 2 (Kahneman, 2011). The Librarian (Sonnet) flags candidates aggressively. The Scholar (Opus) verifies them slowly and decides whether to commit. Cheap-and-broad gates expensive-and-narrow, the way the brain's salience network gates the prefrontal cortex.
- Three retrieval tiers. Humans don't query memory uniformly. They get ambient familiarity-driven priming, deliberate hippocampal recollection, and a fast orbital-PFC "stop signal" when the environment matches a stored constraint (Yonelinas, 2002; Aron, Robbins & Poldrack, 2014). Ultan exposes each as a separate mechanism with its own latency budget (§ Three retrieval tiers, below).
References
- Lisman, J.E. & Grace, A.A. (2005). The Hippocampal-VTA Loop: Controlling the Entry of Information into Long-Term Memory. Neuron, 46(5), 703–713.
- Schultz, W., Dayan, P. & Montague, P.R. (1997). A neural substrate of prediction and reward. Science, 275(5306), 1593–1599.
- Yonelinas, A.P. (2002). The nature of recollection and familiarity: A review of 30 years of research. Journal of Memory and Language, 46(3), 441–517.
- Kahneman, D. (2011). Thinking, Fast and Slow. Farrar, Straus and Giroux.
- Aron, A.R., Robbins, T.W. & Poldrack, R.A. (2014). Inhibition and the right inferior frontal cortex: one decade on. Trends in Cognitive Sciences, 18(4), 177–185.
- Sinclair, A.H. & Barense, M.D. (2019). Prediction Error and Memory Reactivation: How Incomplete Reminders Drive Reconsolidation. Trends in Neurosciences, 42(10), 727–739.
- Collins, A.M. & Loftus, E.F. (1975). A spreading-activation theory of semantic processing. Psychological Review, 82(6), 407–428.
What it does
- Surprise gates the write (see Design, in one paragraph, above). The curator asks, of every candidate, "would a competent assistant produce this advice unprompted?" If yes — already in the model's baseline knowledge, no information added by storing it — skip. If no, capture. Three salience signals trigger a write, mapping directly onto prediction-error, novelty, and reactivation:
- Contradicts an existing entry — user has changed their mind. Highest priority. Deprecates the old, writes the new. (Prediction-error: stored belief was wrong.)
- Novel — not in the library, not derivable from the model's training (user-specific facts, strict overrides of defaults, idiosyncratic preferences). (Novelty: no matching trace exists.)
- Reinforces — user repeated something we already have. No new entry; daemon bumps a
reinforcedcounter on the existing entry to track how often it's reasserted. (Reactivation: existing trace strengthened, not duplicated — Sinclair & Barense, 2019.)
- Use-tracking, not a write trigger. A fourth
salience_signalvalue, used_helpfully, fires when the agent actually relied on a surfaced entry to answer — the Librarian judges genuine reliance (a mere mention is not use; disagreement stays contradicts), and the Scholar deterministically bumps a separatefired-helpfulcounter on the cited entry, deduped per turn so a re-scanned turn never double-counts. It is the positive-evidence half of the prefrontal-inhibition analog (see Roadmap), kept distinct from the three write-gating signals above. (Now consumed by Tier-1 ranking as a gentle usefulness tiebreaker; feeding it into decay resistance is still a TODO — see Roadmap.) - Two-tier curator with asymmetric bars. The Librarian (Sonnet) does fast salience detection — low bar, recall-tuned. The Scholar (Opus) deliberates — higher bar, precision-tuned. System 1 gates System 2; cheap-and-broad gates expensive-and-narrow.
- Organises a real library, not a flat pile. Topical hierarchy emerges from content. Every folder has a README. ≤5 entries per directory before splitting. Auto-maintained child listings between marker comments. Wikilinks validate. Frontmatter validates. Scope/path agreement enforced.
- Slash commands wire it into Claude Code without ceremony:
/ultan <text>— drop something into memory now, no extraction needed./ultan-advisor <question>— query the library before asking the user a preference question. The advisor finds relevant entries (Sonnet, BM25 + embeddings + Read), writes a referenced answer (Opus), and clearly distinguishes stored knowledge from its own opinion. Always cheaper to check than to ask./epiphany [project]— hunt for one non-obvious connection between distant entries the graph never linked (see Epiphany, below). Read-only.
- Pure markdown store. No database. The library is
~/.agent-mem/knowledge/—ls,cat,gitit. Two derived indexes alongside (.bm25.idxfor keyword,.embeddings.idxfor semantic) auto-rebuild on drift.
Three retrieval tiers
The agent can't afford to query the library for everything it's about to do, and humans don't either — retrieval is layered. Familiarity-based priming via spreading activation (Collins & Loftus, 1975); explicit hippocampal recollection (Yonelinas, 2002); and the rapid orbital-PFC "stop signal" that interrupts an in-flight action when it conflicts with a stored constraint (Aron, Robbins & Poldrack, 2014). Ultan implements all three:
| Tier | Cognitive analog | Latency | Trigger | What it does |
|---|---|---|---|---|
| 1. Ambient priming | Familiarity / spreading activation (Collins & Loftus, 1975) | <2s (Unix-socket round trip to warm daemon; budget enforced by the hook) | Hook fires a priming RPC to the daemon on every UserPromptSubmit | Top 3 most-relevant entries injected as additionalContext on every UserPromptSubmit, each with a short body excerpt so the agent can cite the actual rule rather than guess from the title. Bullets carry freshness (★ for entries updated in the last 7 days) and kind markers ([C] convention, [P] preference, [F] finding, [W] warn). Per-session dedup: entries already surfaced this session don't re-surface — once shown, they're in the agent's context. Three-stage retrieval: BM25 + sentence-transformer embeddings (nomic-embed-text-v1.5) → RRF fusion → cross-encoder rerank (ms-marco-MiniLM-L-12-v2), boosted by reinforced counter, project-scope prior (current-project bonus, cross-project penalty), and a gentle fired-helpful/fired usefulness tiebreaker. ≤1500 char budget. |
| 2. Deliberate recall | Hippocampal recollection (Yonelinas, 2002) | 30-60s | /ultan-advisor <question> invocation |
Sonnet Librarian searches deeply, Opus Scholar synthesises a referenced answer. The drill-down when the priming snippet isn't enough. |
| 3. Acute notice | Orbital-PFC stop signal (Aron et al., 2014) | <100ms synchronous | PreToolUse hook on every tool call | Default advise: pattern-matches the tool call against entries with block_triggers; on match, emits an additionalContext FYI — "📚 Library note: [[X]] applies here" — and the tool proceeds. The agent decides. Opt-in severity: block is reserved for genuinely dangerous actions (rm -rf /, force-push to main); only then does the hook hard-deny. Like a human noticing a relevant memory mid-action: noticed, not paralysed. |
The Scholar still owns a soft nudge pipeline orthogonal to these — when the Librarian sees a stored preference matching the rolling buffer, the Scholar can approve a nudge to pending-nudges.md for injection on the next turn. Budget: 1/turn, 3/session.
The library is a graph
The library is structurally a graph: folder hierarchy gives the tree, wikilinks ([[path/to/entry]]) give cross-links that turn it into a DAG. The Scholar maintains both as first-class objects — add_wikilink is one of its action verbs, every folder README's child listing is auto-reconciled between marker comments, and every wikilink is validated on write (the Librarian's proposal is dropped if a link doesn't resolve).
Retrieval over the graph is split across tiers:
- Tier 1 (priming) is text-only — three-stage retrieval: BM25 + sentence-transformer embeddings (nomic-embed-text-v1.5) fused via RRF, then re-ordered by a cross-encoder reranker (ms-marco-MiniLM-L-12-v2) co-attending over each
(query, body)pair, and finally boosted by thereinforcedcounter, project-scope prior (current-project bonus + cross-project penalty), and a gentlefired-helpful/firedusefulness tiebreaker. The top 3 survivors are rendered with body excerpts, freshness markers, and one-character kind tags; per-session dedup suppresses entries already shown to the agent. The retriever does not traverse the graph; it returns top-k entries by relevance, with wikilinks appearing in the output as hints. - Tier 2 (deliberate recall) is agent-driven graph traversal. The agent sees wikilinks in priming context and follows them via the
ultan-searchskill, which returns the entry plus its local neighborhood — siblings, subfolders, parent README — in one read. The agent decides which edges to follow and when to stop, the same "memory as tools" pattern Letta and Wire have shown beats pre-baked retrieval expansion on cross-document queries.
The deliberate choice: deterministic-and-cheap text retrieval at always-on Tier 1, semantic-and-expensive graph traversal at on-demand Tier 2. PageRank-style structural expansion at Tier 1 is a possible addition (see Roadmap), not a current capability.
Epiphany — the connection nothing indexed
The three tiers above retrieve what's relevant to the current moment. Epiphany inverts that: with no query, it goes looking for a genuinely non-obvious connection between two distant entries the graph never linked — and surfaces the single best one. If Tier 1 is spreading activation (what fires near the prompt), this is deliberate divergent–convergent search (what you find when you set out to roam). Read-only; it never writes.
/epiphany # roam the whole library, bridge across domains
/epiphany <project> # roam one project, bridge across its subsystems
The method — map → fan out → converge → judge.
- Map. A stdlib filesystem walk partitions the library (or one project) into regions/subsystems — the territory the scouts divide up. Daemon-independent, so it works during warmup.
- Fan out (blind generation). 5–8 scout agents (Opus) run in parallel, each owning a home region, each bridging out to a distant region for far pairs that share deep structure — same failure mode, trade-off, or mechanism under unrelated surface topics. They are blind to each other: independent diversity is the asset being protected.
- Converge + ground (the agents talk to each other and to the source). Candidates pool onto a shared blackboard; the same scouts continue — building across each other's findings, challenging the weakest, voting — with one standing skeptic whose job is to refute and to demand the source. Crucially, entries are treated as leads, not ground truth: when a candidate rests on a technical claim, an agent follows the entry's pointers out to the actual artifact (the code/file it references), reads it, and refines or kills the bridge by what the source says. Grounding is iterative — a dimension of the discussion, not a final gate.
- Judge ratifies on source-grounded evidence, not vote count. It kills the false positives: already-linked, abstraction-of-its-own-instance, "both are hard" analogies, and — the subtle one — bridges that hold only at the summary level and dissolve against the source. The richest find is often a tension between two of your own entries — but a tension only counts once it survives the artifact.
Neuroscience-inspired. The shape — over-produce diverse candidates, then competitively select one — is a named model of creativity, not a loose metaphor:
| Stage | Cognitive analog | Backing |
|---|---|---|
| Blind fan-out | Blind variation → selective retention | Campbell 1960; Simonton 1999 |
| Over-produce, then prune | Neuronal selectionism ("neural Darwinism") | Edelman 1987 |
| Generator/evaluator split | DMN generation ↔ executive-control evaluation, salience switching | Beaty et al. 2016; DMN↔ECN switching predicts creative ability (Communications Biology, 2025) |
| Converge by message-passing | Recurrent settling to an attractor; global-workspace "ignition" | Hopfield 1982; Dehaene & Changeux 2011 |
| Ground against the source | Active inference — test the prediction against sampled evidence | Helmholtz 1867; Friston 2010 |
| Surface ONE | Winner-take-all broadcast (global workspace) | Baars 1988 |
| Standing skeptic | Lateral inhibition — competition, not agreement | (keeps convergence honest) |
The honest caveats. This mechanizes deliberate creativity, not the spontaneous "aha" (incubation/DMN-driven and non-deliberate). Two failure modes are real and designed-against. Groupthink: agents that talk can herd onto a confident-but-wrong answer — social influence measurably erodes the wisdom-of-crowds effect (Lorenz et al., 2011) — so the first round is blind and a standing skeptic runs throughout. Summary-level mirage: the scouts reason over entry summaries, which are lossy, so a connection can rhyme in the prose yet be false in the artifact. A live research run hit both: five "blind" scouts converged on a cluster the skeptic showed was already cross-linked (the link graph reflected back, not discovery), and the surfaced "contradiction" only collapsed when an agent finally opened the actual Lean source and saw the result was about a single bit, not the multi-digit object the bridge had assumed. The fixes: blind generation + standing skeptic for the first; iterative source-grounding (entries are leads, the artifact decides — active inference, not prior-settling) for the second. Treat the output as a hypothesis, not a verdict.
Development setup
For hacking on Ultan itself, or running from source without the plugin. This is the manual path the plugin automates — you wire the hooks yourself and run the daemon directly from the repo.
# 1. Sync the daemon's deps (uv-managed)
cd daemon && uv sync --group dev
# 2. Sync the search CLI (separate venv, shared BM25 implementation).
# Pulls in sentence-transformers + einops; HuggingFace model weights
# are downloaded on first daemon start (see step 4 below).
cd ../tools/search && uv sync
# 3. Wire the hooks
# The plugin is the supported install path: it registers the hooks (and the
# /ultan, /ultan-advisor commands) automatically from `hooks/hooks.json`.
# Running from source WITHOUT the plugin, mirror the `ultan hook <event>`
# entries in `hooks/hooks.json` into your own ~/.claude/settings.json. One
# daemon per machine serves the whole library across every repo.
# NOTE: don't wire these by hand if you've installed the plugin — the hooks
# would double-fire.
# 4. Start the daemon (foreground; logs to ~/.agent-mem/daemon.log). One per
# machine — it listens on ~/.agent-mem/priming.sock and answers Tier-1
# priming requests from every hook on every project.
cd /path/to/ultan/daemon && uv run agent-mem-daemon -v
# (nohup, tmux, or a launchd plist if you want it persistent — auto-supervision
# not yet implemented.)
# 5. Open Claude Code in any project (no per-project setup needed once the
# hooks are global) and work normally. Entries land under
# ~/.agent-mem/knowledge/ as the Scholar approves them.
How it works (the short version)
flowchart TD
Hooks["<b>hooks</b><br/>UserPromptSubmit · PostToolUse · Stop · SessionEnd"]
Events[("~/.agent-mem/events.jsonl<br/><i>append-only</i>")]
Library[("~/.agent-mem/knowledge/<br/><b>your library</b>")]
Hooks -->|append JSONL per event| Events
subgraph Daemon["agent-mem-daemon"]
direction TB
Tailer["TailerThread<br/>RollingBuffer per session"]
Debounce["DebounceScheduler<br/><i>per-session quiet timer</i>"]
Librarian["<b>LibrarianPool</b> — N × Sonnet <i>(System 1)</i><br/>classify salience: contradicts · novel · reinforces · used_helpfully<br/>propose actions"]
Reinforce["bump <code>reinforced</code> + <code>fired-helpful</code> counters<br/><i>deterministic, no SDK cost</i>"]
Scholar["<b>ScholarWorker</b> — Opus <i>(System 2)</i><br/>'would I produce this unprompted?'<br/>approve + execute · or veto + drop"]
Reconciler["<b>Reconciler</b> <i>(deterministic, post-batch)</i><br/>READMEs · child listings · wikilinks · frontmatter · scope"]
Tailer --> Debounce --> Librarian --> Reinforce --> Scholar --> Reconciler
end
Events -->|tailed| Tailer
Reconciler --> Library
Design discipline that survived live testing:
- The model writes nothing. The curator's tools are read-only (read · grep · BM25 · embedding); the LLM returns a typed, validated set of actions and a deterministic executor is the only thing that touches disk. Paths are checked at the boundary, never trusted from the prompt.
- No silent fix-ups. The Scholar can only approve-and-execute or veto-and-drop. If the Librarian got the path wrong, the proposal is lost — recurs next session if real. Forces the Librarian to be careful.
- Schema as single source of truth. The output models (
ScholarDecisions/LibrarianProposal) are the tool schema the model fills in and the validators it must satisfy. Change the schema and both the model's contract and the boundary checks move with it. - Auto-reconciled READMEs. Every folder's README has a
<!-- ULTAN:children (auto) -->marker block. The LLM writes prose above; the daemon keeps the listing in sync after every batch. No drift. - Typed output over the subscription SDK. The curator runs on your Claude Code subscription via
claude-agent-sdk— never the metered API — yet still gets Pydantic-AI-style discipline, through a small in-house shim (typed_agent.run_typed). The model "returns" by calling asubmit_resulttool whose input schema is the Pydantic output model; a validator runs at that boundary, and anything malformed (an unresolvable wikilink, an over-cap directory, unparseable frontmatter) is handed straight back as the tool result so the model self-corrects in-band. No JSON scraped from free text, no post-hoc repair — bad data never crosses the boundary. Pydantic-AI itself only speaks the metered API, so we kept its ergonomics without its billing. - Persistent tailer offset so daemon restarts resume mid-stream instead of seeking to EOF and losing the events that arrived during downtime.
- No literal secrets in the library. Both curator prompts are explicit: the Librarian must not quote credentials in any field of a proposal (API keys, OAuth secrets, GitHub PATs, AWS keys,
sk-*keys, private-key blocks, connection strings with passwords, JWTs); the Scholar treats the same as a hard-veto invariant. Memory is plain markdown on disk and often git-tracked — defence in depth at both stages.
Prior art
Ultan is forked from and inspired by two earlier projects. Heavy credit to both authors:
- coleam00/claude-memory-compiler — provided the codebase skeleton: hook architecture (
SessionEnd→ flush → daily log → compile), the recursion-guard pattern viaCLAUDE_INVOKED_BY, and the Karpathy-style knowledge layout (daily, concepts, connections, qa, index, log). The whole writes-via-Claude-Agent-SDK pattern withpermission_mode="acceptEdits"and theclaude_codepreset is from here, near-verbatim. - mann1x/claude-hooks — the daemon pattern, the threaded worker model, and the discipline around fault isolation in parallel pools. We cribbed the
ThreadPoolExecutorfan-out from their_parallel.pyand the threading reasoning fromdaemon.py(their explicit choice of stdlibthreadingover asyncio — "hooks are I/O-bound; GIL releases during socket reads; small ThreadPoolExecutor gives near-linear speedup without the rewrite cost" — is exactly what made our parallel daemon tractable).
What we deliberately didn't borrow from claude-hooks: the Qdrant / pgvector / sqlite-vec providers, the API proxy, the dashboard, the LSP engine. Excellent code, all of it — just not what a personal memory store needs.
Layout
agent-mem/
README.md ← this file
ultan/ ← hook runtime + `ultan` CLI (dispatch, doctor, mcp) and
the hot-path modules (events, priming, blockers, nudges)
hooks/ ← hooks.json — the plugin's hook manifest
commands/ ← /ultan, /ultan-advisor slash commands
skills/ ← ultan-search skill
daemon/ ← the long-lived event-ingest daemon
agent_mem_daemon/ ← package
tests/ ← pytest suite (see Status below for current count)
pyproject.toml ← uv-managed
tools/
ultan/ ← /ultan, /ultan-advisor command logic (advisor, remember)
search/ ← `agent-mem search` CLI + BM25 indexer (shared library)
docs/
LIBRARIAN_PROMPT.md ← Librarian role reference
SCHOLAR_PROMPT.md ← Scholar role reference
SCHEMA.md ← entry schema reference
Storage on disk:
~/.agent-mem/
events.jsonl ← append-only; hooks write, daemon tails
daemon.log ← rotated daemon log
daemon.pid ← acquired on start
daemon.offset.json ← persistent tailer offset for restart-safe resume
pending-nudges.md ← Scholar writes, hook reads + clears + injects
cost.json ← running spend tally
runs/<date>.jsonl ← per-call audit (cost, duration, decisions, parsed_ok)
runs/<ts>-<role>-<sid>.md ← full prompt + response transcripts (7-day TTL)
knowledge/ ← your library
README.md
index.md ← catalog
log.md ← Scholar action log (writes + vetoes + reasoning)
global/ ← cross-project entries (Librarian organises sub-topics dynamically)
projects/<slug>/ ← per-repo entries
_archive/ ← archived entries
A note on cost
Ultan is token-heavy by design, and it runs on your Claude Code subscription. The curator (Librarian + Scholar) calls models through claude-agent-sdk — the same auth your Claude Code session already uses — so on Max/Pro the usage counts against your plan's limits, not a metered per-token bill. It trades a generous slice of that quota for richer memory and lower friction. Expect roughly:
- Librarian (Sonnet) runs after each session's quiet period (per-session debounce, default 30s). With moderate activity that's ~10-30 invocations per working day per project. Each is a few thousand input tokens (prompt + library snapshot + buffer) plus a few hundred output tokens.
- Scholar (Opus) runs in batches — every 3 Librarian packets or 60s, whichever first. Each batch is one Opus call: prompt + accumulated proposals, ~30s wall time, a few thousand input + a few hundred output tokens.
- Ambient priming (Tier 1) is daemon-side BM25 + embeddings + cross-encoder rerank, no LLM cost, but it injects up to 1500 chars into every UserPromptSubmit — call it ~400 tokens of prompt overhead per turn.
- Advisor (
/ultan-advisor) is one Sonnet call (Librarian step) + one Opus call (Scholar synthesis) per invocation. - PreToolUse Tier 3 is pure deterministic regex match, no LLM cost, sub-100ms.
On a Max/Pro plan this is real quota: active dogfooding across projects can chew through a meaningful slice of your usage window, and on a busy day you may hit your plan's rate limit. When that happens the affected batch is simply dropped and its lessons recur next session — the daemon never silently falls back to a paid API. (If you've instead pointed the Claude Code CLI at a metered ANTHROPIC_API_KEY, the same volume runs roughly $5-20/day pay-as-you-go.)
If that's too rich for your workflow: turn off the daemon entirely and use just the slash commands (/ultan, /ultan-advisor) — the curator stops running, but the explicit-write and explicit-query paths still work. You lose ambient priming, automatic extraction, and proactive nudges, but you keep the markdown library and the on-demand advisor.
Roadmap
Three known gaps relative to where the bio framing points.
Tier 1 graph signal
Tier 1 priming uses BM25 + embeddings (nomic-embed-text-v1.5) + RRF + cross-encoder rerank (ms-marco-MiniLM-L-12-v2) + reinforced + project-scope prior + a fired-helpful/fired usefulness tiebreaker. One cheap structural signal still sits unused:
- Wikilink-density boost — heavily inbound-linked entries are load-bearing (PageRank's intuition, cheap to compute on a few-thousand-entry library). One extra term added before the cross-encoder rerank (so densely-linked entries get a better chance of surfacing into the rerank's candidate pool).
Doesn't require architectural change — it's a small addition in priming.py before _rerank_candidates. The agent-driven Tier 2 traversal stays as-is; this is purely about giving Tier 1 a structural prior on top of the relevance-only signal the cross-encoder already provides.
Reflective abstraction (offline integration of leaf episodes)
Slice 1 shipped (abstract_entries). Ultan now has a bottom-up pass that synthesises higher-order rules from accumulated leaf entries, closing one of the largest deltas from a mammalian system: hippocampal–neocortical dialogue during offline periods produces transitive inferences and schema abstractions that no single episode contains (Schlichting & Preston 2015; Eichenbaum 2017; Preston & Eichenbaum 2013). The Scholar was reactive — it judged incoming proposals — and is now also reflective: it approves or vetoes net-new parent abstractions the Librarian proposes over related leaves. Decay (PR #7) removes unused noise; reflection consolidates the used patterns that would otherwise stay as N parallel leaves, so the library no longer has only flat breadth — facts can graduate to a higher-order rule. The canonical LLM-side analog is the reflection mechanism in Park et al.'s Generative Agents (2023). A-MEM (Xu et al., NeurIPS 2025) — cited below in the Reconsolidation row — does hierarchical evolution of entries on use; reflection here adds the net-new parent abstraction step that A-MEM doesn't propose.
It is one more verb in the Librarian's vocabulary, not a separate clustering daemon. The Librarian already reads multiple entries and proposes structural changes to the Scholar (merge_entries, split_folder, add_wikilink are all multi-entry-reading-with-judgment actions). abstract_entries is one more verb in that vocabulary. Cosine clustering would force a topical-summary shape (Park et al. 2023's flavour); the agent-driven path gives us the schema-inference flavour the cited Eichenbaum/Schlichting biology actually describes — "rule A about python + rule B about JS → abstract rule 'user likes linting across languages'" (predicts wanting lint in a new language like Rust), not "5 entries about python deps → 1 themed summary." Same pattern as [[projects/agent-mem/conventions/retrieval/no-regex-seed-extraction-use-parallel-search]]: the Librarian is a tool-using agent making judgments, not an algorithm running offline.
This is surprise-gated, not similarity-gated — the same encoding bar the leaf writes use. A proposal is approved only when it's a genuine "aha", an integration event with reward-prediction-error signature, not a topical cluster. The biology is specific: only strongly novel-or-congruent items encode (the "mild middle" is dropped) (van Kesteren et al. 2012, SLIMM); insight is the binding of a remote association (Jung-Beeman et al. 2004); the aha moment is a reward-prediction-error that drives durable encoding (Kizilirmak et al. 2016); and integration is what lets the hippocampal–prefrontal system make a novel inference about an unseen case from separately-stored episodes (Schlichting & Preston 2015; Tse et al. 2007; Behrens et al. 2018). So the gate, enforced in BOTH the Librarian's propose-side prompt and the Scholar's veto-side prompt, has four parts: (1) remote children — leaves from different domains, so the link is non-obvious; (2) predictive lift — the parent enables a confident call on an unseen case no single child supports; (3) non-obvious — a competent assistant wouldn't have stated the rule unprompted; (4) compresses — the rule is shorter than its children and regenerates them. The Scholar treats failing the bar as a precision veto. Explicit must-reject patterns (same-surface groupings like "all about yellow things", vague ones like "likes uv + likes ruff → likes fast tools", true-but-worthless ones like "likes l
No comments yet
Be the first to share your take.