- Five timestamps on every memory, across two clocks
- BM25, dense, index and graph retrieval fused by RRF
- As-of queries: what the agent believed on any date
Memory for long-running agents that knows when a fact stopped being true. Every memory carries five timestamps across two clocks — when the system learned something, and when it was true — so retrieval filters on a validity window instead of returning whatever was written last. A contradiction closes the old record's window instead of piling up beside it. Retrieval fuses four channels — BM25, dense vectors, a page index and a graph walked by personalised PageRank — under a query-dependent weighting.
It is built against three failures of agent memory. Stores that only append, so a fact that has since become wrong stays active forever. Stores that keep time as metadata but never enforce it at retrieval, so the timestamp is decoration. And stores tuned for one retrieval mode, which lose their footing on the query diversity real use produces. Each has a mechanism answering it.
The write path answers the first two. Every memory carries t_created, t_observed, t_valid, t_invalid and t_expired, splitting when the system learned something from when it was true in the world. Writes are one of four operations — add, update, delete or no-op — and a contradiction runs a fixed path: look up related memories in the graph neighbourhood, classify the contradiction, then supersede the old record by setting its t_invalid to the new record's t_observed. Nothing is deleted to make a correction. The old window closes, and retrieval stops seeing it.
The read path answers the third. Four channels run — BM25 over a Lucene index, dense vectors in FAISS, a direct page-index lookup and a graph walk — and their ranks fuse as the sum over channels of 1 / (k + rank + 1). Dense and sparse then mix as alpha * dense + (1 - alpha) * sparse, where alpha reads the query itself: short, lexical, symbol-heavy queries take a lower alpha, long semantic ones a higher. The fused score is multiplied by a tier weight and a retention factor, and an optional reranker adds its own weighted score on top.
Tiers give retention its meaning. A memory sits in a short, mid or long tier, and its retention is exp(-t / S) — t the days since it was last touched, S its accumulated strength — so an old memory that keeps being used decays slowly and one nobody touches decays fast. Promotion keys on age together with reinforcement, demotion on inactivity, and a cleanup pass purges what has expired. The default tier weights are 1.5, 1.2 and 1.0.
The graph channel does what the other three cannot. Neo4j holds memories, entities, episodes and communities, with provenance edges recording which memory updated or extended which, and associative recall runs personalised PageRank from seed entities through a bounded subgraph. That is the multi-hop association keyword and vector search both miss. Without Neo4j configured, the other three channels still answer.
Around it sits a research loop that iterates instead of answering once — plan, search, integrate, reflect — with HyDE query expansion on the way in, Self-RAG relevance and support checks on the way out, checkpoint and resume, and a replay buffer. As-of queries ask what the agent believed on any given date, how a record changed and what differs between two dates, and a bounded mutation context holds at most 32 items by default, benchmarked against 100,000 memories. Evaluation entrypoints cover HotpotQA, LoCoMo, NarrativeQA and RULER, with a RAGAS wrapper.