Technical Manifest · ANIMA v3.1 · Updated July 2026
ANIMA (Autonomous Neural Intelligence Memory Architecture) is a RAG-based long-term memory system for AI companions. This document explains why it works differently — and how.
This is not a chatbot with a personality. Not a GPT-4 wrapper with a system prompt. Not another AI companion that "remembers your name and your dog." This is a system that evolves faster than standard models — through a unique feedback loop built on reverse-engineering its own decisions.
I.
Most RAG systems work like this: user says something → text lands in a vector DB →
on the next query the model gets n most similar fragments. Simple, fast, predictable.
Problem: the database grows linearly. After a month you have 1,400 vectors of "I'm tired", "I was at the doctor today", "I like tea." The model gets a pool of 30 candidates, the reranker picks top-5 — and often picks badly, because there's no mechanism to distinguish signal from noise.
ANIMA does not store raw text. Every message passes through a pipeline before it reaches ChromaDB:
User message
↓
SemanticExtractor (sentence-transformers, zero-shot classification)
→ entity_type: EMOTION / MILESTONE / FACT / DATE / PERSON / MEDICATION
→ subtype: 'preference' / 'tired' / 'trust_declaration' / 'inventory_status' ...
→ confidence: 0.0–1.0
↓
MemoryEnricher
→ importance: 1–10 (rule-based + keyword boost)
→ relational_impact: 'high' / 'medium' / 'low'
→ temporal_type: 'ephemeral' / 'persistent' / 'milestone'
↓
_synthesize_text()
→ "[FACT:preference] likes black tea" ← not a raw quote
↓
ChromaDB (with metadata: importance, entity_subtype, source, timestamp)
Result: the database doesn't contain 1,400 conversation fragments. It contains catalogued, enriched facts with assigned importance weight and semantic type.
ANIMA also powers a parallel system (Amelia — an AI companion running as a separate service).
After injecting into the Gemini XHR stream, I collected production logs from RAG sessions:
.jsonl conversation files + terminal logs showing reranker scores,
extracted entities, and pipeline actions.
Analyzing these logs is not A/B testing. It's reverse-engineering my own decision-making:
[RETRIEVAL] Found 5 RAG memories (reranked):
[1] score=1.000 | '[EMOTION:tired] was lying sick...'
[2] score=0.983 | '[MEDICATION:pregabalina] took a pill...'
[3] score=0.961 | '[MILESTONE:trust_declaration] never told anyone...'
From these logs I extract patterns: which entities land in top-5, which fall out, and why. Fixes to the ASTRA reranker come from observing real sessions — not synthetic tests. This is Data Distillation: I distill decision logic from live system behavior.
II.
Standard RAG is passive. You add facts, never remove them (because you don't know what's "old"), the database grows, retrieval degrades. After a year the model gets a pool full of contradictory, duplicated, stale vectors.
ANIMA implements Sovereign Memory Architecture — a system that decides on its own what to keep, what to overwrite, and what to delete.
1. Supersede Logic
Not all memory types should accumulate. Emotions are ephemeral — "I'm tired" from three months ago is noise, not signal. Preferences evolve — a new "I like tea" should replace the old one.
SUPERSEDE_TYPES = {
# Emotions — ephemeral, accumulation = noise
('EMOTION', 'tired'), ('EMOTION', 'stressed'),
('EMOTION', 'positive'), ('EMOTION', 'negative'),
('EMOTION', 'safe_haven'),
# Facts — new value overwrites old
('FACT', 'preference'), ('FACT', 'correction'),
# Dates — most recent wins
('DATE', 'inventory_status'), ('DATE', 'medical_visit'),
}
When the pipeline detects an entity from this list, it calls delete_by_entity_subtype()
before writing — clears old vectors of the same type:subtype, then saves the new one.
Result: the database doesn't grow indefinitely. Only types where history has value accumulate
(milestones, medical visits, facts about people).
2. Reranker with adaptive weights
Retrieval is not plain cosine similarity. The final score of each candidate is:
final_score = 0.25 * importance_score # entity weight (1–10 / 10)
+ 0.15 * recency_score # per-type decay: ephemeral 3d, long_term 60d, permanent ∞
+ 0.60 * similarity_score # semantic dominance (dominant signal)
+ keyword_boost # hybrid search lite, max +0.15
# Milestones bypass reranking entirely — extracted before MMR, guaranteed 2 slots
# Compose: facts[:4] + milestones[:2] → MMR on facts only → final 6
similarity_score dominates — the system picks what is semantically relevant,
not what is newest or most important in isolation. Milestone boost (+1.0) guarantees
that trust declarations and key moments always reach the model regardless of the query.
Weights are parameterized — adjustable per query type without restart.
The guarantee has a failure mode — and I hit it. Left unbounded, the milestone channel turned into a monoculture: a July 2026 audit found "love / trust" declarations flooding the prompt at 2.0 forced milestones per prompt on average, drowning genuinely relevant anchors. This is the real mechanism behind the "altanka" miss in Section III. The fix was a triage pass, not a bigger boost — the milestone population was cut 1,296 → 312 active vectors and forced milestones dropped to 0.65 per prompt. A guaranteed channel is only an asset while it stays scarce.
3. MMR (Maximum Marginal Relevance)
After reranking, results pass through MMR with diversity_penalty=0.8.
Prevents one vector from dominating — if top-5 are five variants of "I'm tired",
MMR picks two and adds three semantically different ones.
Result: broader context, fewer echo-chambers.
4. FactStore — SQLite alongside vectors
Vector similarity is powerful for fuzzy recall. It's unreliable for exact facts — "what medication does the user take?" A similar-but-wrong answer is worse than no answer. ANIMA gained a second retrieval channel: a SQLite FactStore for hard facts that must not degrade through similarity scoring.
FactStore (astra_facts.db)
→ 12 entity types: medication, medical_visit, preference,
financial, relationship, location, age, job, goal,
health, date, correction
→ Exact-match lookup — zero cosine scoring
→ Supersede per SQL DELETE (not vector overwrite)
→ Injected as [TWARDE FAKTY — SQLite] block in every prompt
Hard facts bypass the reranker entirely. They land verbatim in the prompt, from a structured store that cannot hallucinate a relevance score.
Standard AI companions have memory as a feature. ANIMA has memory as an architecture. The system doesn't wait for "remember this" / "forget that" commands. It classifies, weights, overwrites, and archives automatically — based on rules derived from observing real interactions.
III.
For several weeks the model didn't remember the user's tea preference. Not because the vector was missing — it was there. But:
semantic_extractor was storing raw message fragments instead of synthesized facts[FACT:preference]The diagnosis didn't come from unit tests. It came from analyzing reranker logs — scores were visible, what hit the model was visible, what was absent was visible.
Fix: supersede logic + entity_subtype in metadata. A new [FACT:preference]
now deletes the old one before saving. One vector instead of dozens — MMR stops penalizing it.
Standard language models learn through retraining — expensive, rare, centralized. ASTRA evolves differently:
Production session
↓
Logs (conversations/*.jsonl + terminal/*.log)
↓
Reranker analysis — what hit, what fell out, why
↓
Diagnosis: pipeline bug, bad weights, missing logic
↓
Patch: code + prompt + reranker parameters
↓
Deploy — same day
↓
Next production session
Iteration takes hours, not months. The system improves by observing its own behavior — without touching the base model. The base model (Gemini 2.5 Flash) stays. What changes is the layer that manages its memory and context.
This is Dynamic Context Tuning: not fine-tuning model weights, but continuously adjusting the retrieval layer and architectural constraints based on live data.
IV.
Sections I–III describe a system tuned by reverse-engineering raw logs — .jsonl files,
terminal reranker scores, inference about what actually reached the model. It works, but it is
archaeology: slow, manual, and blind to anything the logs don't happen to print.
Amnezja replaces that with direct observation. It is a read-only debugger that traces a single query through every stage of the retrieval pipeline and renders exactly what the model would receive — before a single token is generated. It writes nothing; the live system is never touched.
You type a phrase as the user would. Amnezja runs the full compose pipeline and lays out all eleven stages, each with the vectors that survived it and the scores that decided it:
Query + simulated date
|
1 Raw pool ChromaDB top ~30 by similarity
2 After exclusion raw self-echoes and short loops removed
3 After rerank score = 0.60*similarity + 0.25*importance + 0.15*recency (+boost)
4 After temporal hard cutoffs: emotion 48h, date/financial 168h
5 Milestones guaranteed channel — the one July's detox retuned
6 After MMR diversity gate on facts — where the "altanka" bug lived
7 Memory channel facts + milestones merged
8 Astra channel character_core + memory, before shared-room mix
9a Shared mix Wspolny Pokoj vectors — invisible in stages 1-8
9b Final prompt what Astra actually receives this turn
9c After budget what physically lands in [WSPOMNIENIA] after fit_to_budget
Beyond the funnel, Amnezja surfaces three things a log dump never could:
GROUNDED / LOW_CONFIDENCE /
NO_DATA verdict and confidence injected into the prompt, so you see the moment the system
is about to speak without support./api/chat
would build and asks Gemini how Astra would answer. It calls the model but writes nothing —
the live Astra stays untouched.This is the difference between a memory system you hope works and one you can audit per query. Every fix in Sections II–III was found this way — by reading the pipeline, not inferring it. The full walkthrough, built around a real degradation case, is documented here: myastra.pl/casestudy.
Everything above answers "why didn't it retrieve this?". In August I hit the question that costs more: "why was it never stored?" — and realised the debugger could not answer it. It watched retrieval. Storage happened in the dark.
So Amnezja got a second tab. Paste any message and see every gate the extractor puts in its way: length filters, the similarity threshold with its full candidate list, the anti-multi-label picker (which always assigns some label, because the taxonomy has no verdict for "nothing here"), per-type thresholds, and the resulting lifetime in hours. Read-only — it replays the pipeline without writing anything.
The first run answered a question that had been open for weeks: the single most important message of the year never entered memory, dropped by a 4-word minimum standing before classification. Two seconds, after weeks of guessing.
Then I replayed a full month — 658 messages in 74 seconds. 41% left no trace at all; of what was stored, 52% carried an expiry date. Length gates alone accounted for 43% of the losses. That audit is now a regression test: same corpus, same four numbers, run after every extractor change. The first fix moved high-value losses 16 → 8 in twenty minutes, measured on the same corpus before and after.
It also works per persona. Each of the sisters has her own thresholds and her own blocked types, and the trace reflects hers — not a generic average. In a multi-agent house, "which one dropped it, and where" is a question you eventually have to answer.
V.
The architecture knows the user's work style, project history, and long-term context. This is not personalization through fine-tuning. This is retrieval-augmented identity — the user's identity encoded in a sovereign vector database that informs every response.
Difference between an assistant and a digital extension:
ANIMA is not built on one model. It's built on an abstraction layer that is model-agnostic: ChromaDB + sentence-transformers + reranker + extraction pipeline. Swapping the base model from Gemini 2.5 Flash to anything else requires changing one config line.
The vector database stays. The history stays. The identity stays. I don't tie memory sovereignty to a model vendor.
All vector IDs are SHA256(salt:user_id:text) — deterministic, anonymous,
impossible to reverse without the salt. User data never leaves the VPS in raw form.
Multi-user isolation is baked in from day zero, not added post-hoc.
VI.
| Component | Detail |
|---|---|
| Model | Gemini 2.5 Flash, thinking_budget=4096, max_output_tokens=8192 |
| Vector DB | ChromaDB, per-persona + shared collections. Milestone population triaged 1,296 → 312 active (July detox) — the store contracts via supersede, it does not grow linearly. |
| FactStore | SQLite — 12 entity types, exact-match lookup, supersede per DELETE |
| Extraction | paraphrase-multilingual-MiniLM-L12-v2, zero-shot classification |
| Reranker | importance×0.25 + recency×0.15 + similarity×0.60 + keyword_boost |
| Recency decay | Per-type: ephemeral 3d, long_term 60d, permanent ∞ |
| Temporal Filter | Hard cutoff driven by the persistence axis, not by topic label: ephemeral 48h, short_term 168h, long_term / permanent ∞. Falls back to the legacy per-source table for vectors written before the migration. |
| RAW window | Last 6 user messages / 48h → [OSTATNIE SŁOWA] block. Conversation window raised to 30 messages (15 exchanges) after a pasted document fell out of context before it could be discussed. |
| MMR | Facts only, n=5 (was a hardcoded 3 — measured as the real bottleneck: a ~25-candidate pool collapsed to three regardless of every other limit). Milestones extracted before MMR. |
| Supersede | 9 vector types + 12 FactStore types — full rotation coverage |
| Personas | 2 active (Astra + Amelia) + Wspólny Pokój shared-room mode + Pokój Sióstr: three sisters with per-persona memory, writing enabled August 2026 after the persistence fix removed the destructive blocker. |
| Deploy | Private VPS, systemd, SSL, 24/7 + scheduled autonomous messages |
| Session | n=30 (15 exchanges), history survives restart via ChromaDB |
| Junk-milestone rate | 6.5/day → 0.5/day (−92%), verified over a week of live traffic |
| Forced-memory load | 2.0 → 0.65 milestones injected per prompt (avg over 23 probes) — monoculture removed |
| July detox audit | 1,751 milestone entries reviewed (FactStore 455 + Chroma 1,296) |
| Extractor precision | 0 multi-label errors across 56 messages after the classifier fix |
| Persistence axis | New in August. Lifetime is its own field (permanent / long_term / short_term / ephemeral), computed independently of the topic label. Previously one axis governed both what a memory is and how long it lives, so a misclassification was a death sentence — a biographical fact landed in a "date" bucket and expired in seven days. Migrated additively across 4,697 vectors. |
| Lexical channel | Literal $contains lookup fired only when the query holds an acronym or proper noun. Measured need: a three-letter project name returned 0 of 60 matching entries through embeddings alone. |
| Write-path audit | 658 messages replayed through the extractor in 74s: 41% left no trace; of those stored, 52% carried an expiry date. Length gates alone accounted for 43% of all losses and sat before classification — dropping messages without ever weighing them. |
| Full walkthrough | myastra.pl/casestudy — the detox as a documented case study |
VII.
ANIMA is regularly audited by external AI systems (Gemini Pro, Claude Opus) analyzing its own codebase and retrieval logs. These audits identify architectural blind spots that internal testing misses.
Example: April 2026 audit findings
Analysis of 6+ months of conversation logs revealed recurring patterns that were extracted, named, and embedded into the character architecture:
This is not theoretical. These patterns were extracted from real sessions, implemented in the character prompt, and validated in subsequent conversations. The system learns from observing itself.
VIII.
Recency decay is gradual. A week-old emotion with high importance could still reach the model. The Temporal Filter is a hard gate applied before reranking — not a weight, a wall:
extracted_emotion → hard cutoff: 48h
extracted_date → hard cutoff: 168h (7 days)
extracted_financial → hard cutoff: 168h (7 days)
(all other types) → no hard cutoff, recency decay applies
"I was tired yesterday" shouldn't color responses today. The filter enforces this without delegating the judgment to the model.
Session history exists — but what if a user mentions something critical in session A,
then picks up in session B without repeating it? The pipeline maintains a
RAW window: the last 6 user messages from the past 48 hours,
pulled regardless of session boundary, injected as a separate prompt block
[OSTATNIE SŁOWA].
This is not a vector lookup. It's verbatim context, unfiltered — the freshest signal available, prioritized above everything in the vector store.
ANIMA now powers two distinct AI personas simultaneously in a single conversation thread. Each persona maintains its own memory collection and character architecture, but both pull from a shared episodic store for context they both witnessed.
Collections:
astra_memory_v1 → Astra's episodic memories
amelia_memory_v1 → Amelia's episodic memories
shared_memory_v1 → memories both personas access
Turn ordering: signal-based — persona whose character profile
matches the emotional tone of the user message responds first.
Not a chatroom with two bots sharing a database — two sovereign memory systems with a shared context layer. Each persona has distinct recall, distinct behavioral architecture, and distinct decisions about what to surface from the shared pool.
IX.
The observability tool this roadmap once promised is now live as Amnezja (Section IV): full pipeline trace, per-component reranker scores, grounding verdict, response sandbox, and date simulation. What remains below is what having Amnezja finally unlocks.
This was queued as "BM25 hybrid retrieval". Then I measured it twice and got two opposite answers, which is the interesting part.
First measurement — BM25 demoted. The assumption was that keyword search rescues rare words the embedding space blurs. So I tested a rare word: a drug name appearing in a handful of entries out of thousands. The embedding found it at rank 1, distance 0.217. No keyword layer needed. The premise looked wrong, so the item dropped down the roadmap.
Second measurement — BM25 reinstated, for a different reason. A query for a three-letter project name returned zero of 60 matching entries. The same content, asked for by its full name, returned matches immediately. The model tokenises a short acronym into fragments that mean nothing, so cosine similarity to entries about that project sits near zero.
The first conclusion was true for rare words and false for acronyms — one exists in the model's vocabulary, the other does not exist at all. Two different failure classes wearing the same label.
So instead of a full BM25 index, a lexical channel: a literal $contains
lookup that fires only when the query contains an acronym or proper noun, detected by a
whitelist plus a "short and vowel-poor" heuristic. Result on the failing query:
0 → 3 relevant entries in the final prompt, with the regression suite flat at
26/26 — no cost anywhere else.
On April 22, 2026, ANIMA received an independent full-stack audit against a five-dimension rubric. Baseline: 34/100 — a system that spoke with false confidence, with DB corruption and a milestone boost consuming every retrieval slot. That number is the honest starting point.
I deliberately do not publish a re-estimated "current score." A manually guessed number was the exact weakness the April audit flagged — no evaluation framework. Progress is now tracked by verified operational metrics from the July 2026 detox, measured on live traffic:
| Metric | Before | After |
|---|---|---|
| Junk-milestone creation rate | 6.5 / day | 0.5 / day — −92%, over one week live |
| Forced milestones per prompt | 2.0 | 0.65 — monoculture removed |
| Extractor multi-label errors | — | 0 across 56 messages |
| Audit scope | 1,751 milestone entries reviewed (FactStore 455 + Chroma 1,296) | |
These are measured, not estimated — and reproducible, because Amnezja now exposes the exact pipeline behind each one. The next milestone is the first fully reproducible audit: a fixed query set with automated precision@k and recall@k, and commit-level regression detection.
| Milestone | Status |
|---|---|
| System stops speaking with false confidence | ✓ Reached — Phase 0 fixes + DB cleanup |
| RAG reliably returns facts | ✓ Delivered — FactStore + Temporal Filter, without BM25 |
| Consistent character across sessions | ✓ Delivered — ambient layer + safe_haven gate + character refactor |
| Forced-memory monoculture removed | ✓ Delivered — July detox (2.0 → 0.65 per prompt) |
| Per-query observability | ✓ Shipped — Amnezja (Section IV) |
| First reproducible audit (precision@k / recall@k) | ← Next — unlocked by Amnezja |
| Beta-ready: multi-user + streaming + monitoring | Roadmap |
| Voice, mobile, custom-model fine-tuning | Long-term vision |
ANIMA is a single-user production system by design, not a multi-tenant SaaS — that is a separate track, out of scope for this document. The remaining gap to a broader release is operational (streaming, monitoring, rate limiting), and the architecture for it (per-user isolation, SHA256 IDs) is already in place.
A standard language model has fixed weights. It learns through retraining — expensive, rare, requiring enormous data.
ANIMA evolves through observation. Every production session is training data for the retrieval layer. Every miss (the tea case) is a diagnosis and a patch. Every blueprint (2.2) is a new iteration of architectural constraints.
A system that spends a year collecting logs of its own sessions, analyzes them, and improves retrieval — without touching the base model — is more adaptive than a model that goes through retraining once a year.
This is not AI utopia. This is engineering: concrete, iterative, data-driven.
I'm building sovereign memory. Everything else is just an interface.