From work on Ashley and Greppa, mid-2026.
I started Ashley the usual way: summarise chats, embed them, retrieve when the next message “looked similar,” keep stable facts in a DB. The model would stop starting from zero.
That gave me storage. It did not finish the problem.
Every turn the app still has to choose which facts, episodes, tools, and instructions the model actually sees. Too little → useless. Everything → noise and wrong continuity.
DB = what exists. Context = what this invocation is allowed to see.
Split the processes
Ashley is two processes:
ashley-core (Rust) — durable facts, turns, episodes, agenda, background work. No model calls.
ashley-agent (TS/Bun) — retrieve, tools, prompt, generate, write back.
Crash the agent, memory stays. Change the model provider without rewriting the store. The model only knows what crossed the socket for this turn.
const [recall, profile, agenda, personality] = await Promise.all([
bridge.send({ op: "recall", query: userMessage, top_k: 8 }),
bridge.send({ op: "get_core_profile" }),
bridge.send({ op: "get_agenda" }),
bridge.send({ op: "get_personality_profile" }),
])
Fetched in parallel. Not treated the same.
Different stuff, different rules
I used to say “memory” like it was one bag. It is not.
Identity (name, timezone, role) goes on every substantive turn. You should not need the message to embed near “timezone” for the agent to know it.
Episodes compete for a fixed budget (~1200 tokens). Situational history only.
Recent turns bridge the gap before summarisation has run.
Preferences change tone/delivery, not “what is true.”
When the budget is tight, episodes die before the profile. That is a product choice, written into the assembler — not a tokenizer accident.
When retrieval made things worse
Early Ashley ran full recall on “hi” and “thanks.”
A greeting would surface an old codebase chat. The model kept working on it. Retrieval was “relevant.” The product was wrong.
Trivial turns now get almost nothing: no episodes, no agenda, empty tool set, skip reflect/critic. Withholding tools mattered as much as withholding data — give a model a retrieval tool and a greeting and it will find something to retrieve.
Precision is not similarity. It is whether loading this improves the next answer.
Check claims after generation
A draft can blend a real fact with something invented. On heavier personal answers, a second pass marks claims as verified / contradicted / invented against store. Tool-grounded answers (files, screen, web) skip that judge.
Loop: retrieve → assemble → draft → check → fix. The draft reveals what needed verification.
Edges I am not proud of yet
- Token budgets still use character fudge factors
- Facts lack good provenance / expiry
- Summarisation can promote bad extractions into “truth”
- Reflect fails open if the judge is down
- Ashley still uses Memvid in places I do not fully trust after Greppa
What I want next is observability: which sources ran, what got in, what was dropped, which claims passed the check. Without that you only see the final prompt and guess.
Greppa’s first question is different
Ashley is one person. Greppa is many.
Before relevance, you need whose memory is this? A shared index + tenant filter works until one path forgets the filter.
Greppa uses one file per scope:
scopes/{scopeId}/memory.sqlite
orgs/{orgId}/memory.sqlite
Another user’s data is not filtered out after search. Their file never opens.
Checkpoint sits on top: hydrate from object storage, immutable generation for readers, ETag CAS for writers. Loser re-runs the mutation on the winner instead of clobbering. Checkpoint only hands you a path — it does not care the file is SQLite, which is why I could swap engines underneath without rewriting it.
Measure the engine
Memvid reported a healthy vector index with no vectors. “Semantic” was keyword for months. My tests shared vocabulary with the target docs, so they passed.
I also under-configured embeds on write — my bug. Migration still happened for structural reasons: file size growing with write count (350 short notes → 16.5 MB, no vectors), closed binary, hangs with no timeout.
SQLite + FTS5 + sqlite-vec + RRF on the same corpus: 80s → 0.18s ingest, real embeddings, smaller file. Tests now use queries with no shared words with the document. That cannot pass on keyword alone.
A dependency that reports success while doing nothing is worse than one that crashes.
Short version
Larger context windows do not decide what is current, safe, or relevant.
Memory is what the system might know. Context is the smaller argument for one call.
Ashley made assembly visible. Greppa made ownership a storage property. If you cannot inspect and bound that decision, you do not have reliable memory — you have storage nearby.
References