Memory
karani retains what it learns and uses it at decision time. One invariant holds the whole design together: the ledger is the system of record, and the semantic index is derived and disposable.
karani remember "Their screen asked about incident ownership" \
--kind question --company "Northwind Robotics"
karani recall "incident ownership" --limit 5Three layers
The ledger — always on, deterministic
Two kinds of ground truth in Postgres.
Structured events the pipeline already keeps: verdicts, outcomes, stages, fit scores, prompt versions. These feed the few-shot block in the qualify prompt and the conversion funnel directly. They are memory, just not phrased as prose.
The memories table: distilled, human-readable facts, one per row.
Append-only with soft deletes, and an exact-duplicate write dedupes to a
timestamp touch rather than a second row.
| Kind | Example | Written by |
|---|---|---|
preference | “Verdict skip on a fintech backend role at fit 82” | automatically, on a verdict |
outcome | “Application outcome offer at Northwind Robotics” | automatically, on an outcome |
company | “Their recruiter replied within three days” | you, or an agent |
strategy | “Platform positioning beat the causal-ML angle for infra roles” | you, or an agent |
question | “Their screen asked about incident ownership” | you, or an agent |
The semantic index — optional
KARANI_MEMORY=mem0mem0 over pgvector, layered on the ledger. Every non-deduped write also goes to mem0, which handles extraction, conflict resolution and embedding. Recall becomes semantic search: “how fast do handbook-first companies reply?” finds the recruiter fact without sharing a keyword with it.
Extraction and embeddings run on a local model, so the memory layer costs zero tokens.
The vector store reads its own DSN, separate from DATABASE_URL on
purpose: the ledger can live in a managed Postgres while the disposable
index stays local.
Working context — per decision
At decision time, karani builds a query from the company, the title and the role category, retrieves the top few facts (five by default, hard-capped at ten in the prompt), and renders them as a block with an explicit instruction: memories are context; they never override a dealbreaker in the posting itself.
That shipped as its own prompt version, so memory-influenced qualifications are distinguishable from pre-memory rows in the funnel. The memory layer’s own effect is measurable rather than assumed.
The invariant
Drop the vector collection and nothing is lost.
Recall degrades to deterministic keyword matching until the index is
rebuilt. Any mem0 failure — extra not installed, local model down, vector
store unreachable — logs a warning and falls back to basic. Writes still
land in the ledger.
A memory outage can never lose data and can never kill a qualify batch (ADR 0009).
karani reindex # replay the ledger into a fresh indexWhen memory is written
- A verdict — a
preferencefact with the fit score, scoped to the company. - An outcome — an
outcomefact. The strongest signal class there is. - Deliberate teaching —
karani remember, or the MCPremembertool. This is the API for “an agent learned something about this company somewhere else and is telling karani.” - A correction — soft-delete the wrong fact and write the corrected one as a new row. The history stays auditable.
Never auto-extracted from arbitrary model output. Writes happen on explicit events or deliberate teaching, so junk cannot accumulate silently — which is the failure mode of every memory system that extracts from everything.
When it is read
- Qualification, single-turn and agent mode alike: per-posting recall, injected as a prompt block.
- On demand:
karani recall, or the MCPrecalltool.
Scoping
Company-scoped recall returns that company’s facts plus the unscoped ones, because a global preference applies everywhere. Another company’s facts are excluded.
Deterministic ranking is stopword-filtered, word-anchored token overlap, with a boost when the memory’s company appears in the query and recency as the tiebreak. That is what the tests run against, which is why it is deterministic rather than good.
Modes
KARANI_MEMORY=off | basic | mem0 # default: basicbasic works on the in-memory store with no infrastructure at all. For the
full stack:
karani infra up --profile local-llm
uv sync --extra memory
uv run python -m spacy download en_core_web_sm
KARANI_MEMORY=mem0What memory is not for
- Not a cache of job descriptions. The jobs table already is one.
- Not auto-extraction from every model response.
- Not a place for secrets or credentials, ever.
- Not fine-tuning by another name. Memory is few-shot context plus measurement, and the verdict loop is the eval.