<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sovantica</title>
    <description>The latest articles on DEV Community by Sovantica (sovantica).</description>
    <link>https://dev.to/sovantica</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14003%2Fe8899677-5828-474a-b064-07c827fa7c2a.png</url>
      <title>DEV Community: Sovantica</title>
      <link>https://dev.to/sovantica</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sovantica"/>
    <language>en</language>
    <item>
      <title>Five things to check your agent memory store against</title>
      <dc:creator>Przemek Marzec</dc:creator>
      <pubDate>Tue, 01 Sep 2026 10:02:38 +0000</pubDate>
      <link>https://dev.to/sovantica/five-things-to-check-your-agent-memory-store-against-427k</link>
      <guid>https://dev.to/sovantica/five-things-to-check-your-agent-memory-store-against-427k</guid>
      <description>&lt;p&gt;An agent-memory bug can look nothing like one. It looks like confidence: the agent remembers an old address, keeps a preference the user changed months ago, or retrieves a fact that should have expired weeks back. Nothing crashes. The answer is just wrong - and the reason is buried in a store that kept growing because every write looked harmless at the time.&lt;/p&gt;

&lt;p&gt;This article is about that layer: what goes wrong &lt;em&gt;after&lt;/em&gt; agent memory starts working. Five failure modes show up often enough to be worth checking any store against, so each one below carries the question to put to yours. They're not equally bad - three of them are correctness problems, the fourth is a security one - and I'll be honest about which parts a memory layer can actually fix versus merely make visible.&lt;/p&gt;

&lt;p&gt;One bias up front, so you can discount for it: Engrava (the library I work on) has no LLM in its write path. That's relevant because when nothing generative runs on write, what the store does is decided by code you can read - so I can be specific about where it breaks, and, where it can, design against that in the data model rather than hoping a bigger model smooths it over. Where it &lt;em&gt;can't&lt;/em&gt;, I'll say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Hallucinated metadata
&lt;/h2&gt;

&lt;p&gt;You ask the store for "high-confidence facts about the user" and get back entries tagged with confidence scores or categories that were never true - invented somewhere on the way in.&lt;/p&gt;

&lt;p&gt;This one is specific to a design choice: running an LLM &lt;em&gt;on write&lt;/em&gt;, to extract entities or tag records. It's convenient, and it adds a second place for the model to hallucinate - except now the hallucination is persisted as structured metadata you'll trust and query against later. The bug isn't in retrieval; it's baked into the row. So the check is narrower than whether an LLM touches the write path at all: it's whether model output gets persisted as fields you later query against - and if it does, what validates it before it lands, and whether anything marks it as model-written.&lt;/p&gt;

&lt;p&gt;Engrava sidesteps this by not having a model in the write path at all. A &lt;code&gt;Thought&lt;/code&gt; is a frozen, validated record - you don't mutate it in place, you call &lt;code&gt;evolve()&lt;/code&gt; and get a new validated instance. Whatever metadata exists was written by your code, deterministically, or it isn't there. You can still &lt;em&gt;use&lt;/em&gt; an LLM to decide what's worth storing; the storing itself is plain code you can read. It's a smaller capability than "the memory understands your data," and that's the point: the store is not inventing any of it. What your code hands it can still be wrong, and Engrava will store that faithfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The contradiction that looks like confidence
&lt;/h2&gt;

&lt;p&gt;A user moves from Berlin to Lisbon. They tell the agent. Six weeks later the agent tells a colleague they're in Berlin - cheerfully, with no hint anything is wrong.&lt;/p&gt;

&lt;p&gt;The first time you see this in a real agent, it doesn't look like a database bug. It looks like the model being strangely sure about something that used to be true. What actually happened is duller: both facts are in the store, nothing recorded that the second &lt;em&gt;replaced&lt;/em&gt; the first, and the retriever picked the older one because it scored higher. In an insert-only store a new fact doesn't retract an older one; they just coexist, and "which one is true now" is information nothing in the store wrote down.&lt;/p&gt;

&lt;p&gt;Mem0's &lt;a href="https://github.com/mem0ai/mem0/issues/4896" rel="noopener noreferrer"&gt;issue #4896&lt;/a&gt; names this directly - &lt;em&gt;"ADD-only architecture doesn't implement conflict resolution for semantically similar memories"&lt;/em&gt; - and was closed as &lt;code&gt;not planned&lt;/code&gt;. A &lt;a href="https://github.com/mem0ai/mem0/issues/4536" rel="noopener noreferrer"&gt;related issue #4536&lt;/a&gt; asks for contradiction handling on the add path. (This doesn't mean Mem0 is broken - it means contradiction becomes a real product surface once agents run long enough, and it's genuinely hard.) So the thing to ask about your own store is what happens on the second write: does it keep both and let ranking sort it out, or does something record that one replaced the other?&lt;/p&gt;

&lt;p&gt;Engrava's answer is to make "true &lt;em&gt;when&lt;/em&gt;" part of the model. 0.4.0 added &lt;strong&gt;bi-temporal valid-time&lt;/strong&gt;: each fact carries not just when it was written but when it held in the world. You don't delete Berlin - you set its &lt;code&gt;valid_until&lt;/code&gt; to the move, and Lisbon's &lt;code&gt;valid_from&lt;/code&gt; from it. Ask "as of now" and you get Lisbon; ask for the history and the supersession is right there, auditable. Keep the record, move the truth.&lt;/p&gt;

&lt;p&gt;Two limits on that, because it is easy to read more into it than it says. Engrava gives you the representation and the history; it does not notice the contradiction for you. If nothing tells the store that Lisbon supersedes Berlin, both facts sit there and "as of now" answers as of a timestamp, not as of the truth. And detection is not a problem nobody has taken on: cognee, for one, ships a &lt;a href="https://github.com/topoteretes/cognee/blob/v1.5.3/cognee/tasks/graph/detect_contradictions.py" rel="noopener noreferrer"&gt;contradiction check&lt;/a&gt; as an optional step in its ingest pipeline. That is a real capability and we don't have it. Whether you want it is the trade-off from section 1 again, because the check is a model pass on the write path.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Memory that grows because every write looked harmless
&lt;/h2&gt;

&lt;p&gt;Six months in, the store has piled up observations that mattered for exactly one turn and never again. The signal you want is still in there, outnumbered by them.&lt;/p&gt;

&lt;p&gt;The same ask keeps resurfacing across frameworks (&lt;a href="https://github.com/huggingface/smolagents/issues/901" rel="noopener noreferrer"&gt;smolagents #901&lt;/a&gt;, &lt;a href="https://github.com/agno-agi/agno/issues/2500" rel="noopener noreferrer"&gt;Agno #2500&lt;/a&gt;, &lt;a href="https://github.com/letta-ai/letta/issues/957" rel="noopener noreferrer"&gt;Letta #957&lt;/a&gt;), and the honest summary is: memory grows unbounded unless something prunes it, and real consolidation is hard. It's worth finding out what in your store ever takes a record back out of the retrievable set - an expiry, a state you can set, a pass that runs on its own - and whether that is the store doing it or you remembering to. I won't claim Engrava has &lt;em&gt;solved&lt;/em&gt; forgetting - it hasn't.&lt;/p&gt;

&lt;p&gt;What it does give you is a lifecycle you can act on. Every thought has an explicit, enforced state - &lt;code&gt;CREATED -&amp;gt; ACTIVE -&amp;gt; DONE -&amp;gt; ARCHIVED&lt;/code&gt;, with invalid transitions rejected at the type level - plus an optional TTL (&lt;code&gt;expires_at&lt;/code&gt;) for things that should age out on their own. That's not "better retrieval"; it's &lt;em&gt;control&lt;/em&gt; - a structural way to say "this is working memory," "this is archived," "this expires Friday," so the store reflects what's live instead of everything it ever saw. (There's also a deterministic background consolidation pass; what it's actually worth is a benchmark question, and I'm not going to wave a number at you I haven't published - that's its own article.)&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cross-agent poisoning - the one that's actually dangerous
&lt;/h2&gt;

&lt;p&gt;The first three are correctness problems, and what they cost you depends on what your agent is for. This one is different in kind: it turns into a security problem the moment two things you don't equally trust write into the same store.&lt;/p&gt;

&lt;p&gt;Run a fleet of agents that don't equally trust each other over one unscoped store, and a single bad actor - buggy, adversarial, or just wrong - writes data the rest of them can retrieve. One weak link, and the bad record is sitting in the namespace they all read from. CrewAI's &lt;a href="https://github.com/crewAIInc/crewAI/issues/2584" rel="noopener noreferrer"&gt;#2584&lt;/a&gt; asked for memory distinguished by a custom key and was closed &lt;code&gt;not planned&lt;/code&gt;; in that design the caller stays responsible for scoping. That isn't CrewAI being careless - a framework that hands you the store and lets you scope it yourself is a defensible position, and it becomes a problem only when the agents sharing that store stop trusting each other. Where there is no first-class boundary between them, that is where memory poisoning lives. Some hosted memory products have added per-resource authorization since; if you are on one, find out what it does by default. The question isn't whether your store can be scoped, it's what separates two agents that don't trust each other before anyone has scoped anything.&lt;/p&gt;

&lt;p&gt;Engrava puts that boundary in the data model. &lt;code&gt;EngravaManager&lt;/code&gt; hands each service its own &lt;code&gt;.db&lt;/code&gt; file - separate store, separate journal, nothing mutable shared between them. One agent's writes don't land in another agent's store unless you wire them together deliberately. Be precise about what that buys you: it is a boundary in the data model, not an access-control system. Two processes that can both read the directory can both open both files, and file permissions are still yours to set. It also won't stop an agent from poisoning &lt;em&gt;its own&lt;/em&gt; memory. What it does is keep one agent's bad writes out of the others' stores by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The black box - "why did it remember &lt;em&gt;that&lt;/em&gt;?"
&lt;/h2&gt;

&lt;p&gt;An agent surfaces an irrelevant memory at the worst possible moment and you're left with two questions you can't answer: what's actually in the store, and why did the retriever rank &lt;em&gt;this&lt;/em&gt; over that?&lt;/p&gt;

&lt;p&gt;If a memory layer gives you &lt;code&gt;add()&lt;/code&gt; and &lt;code&gt;search()&lt;/code&gt; and not much in between, the tracing and the observability have to be built &lt;em&gt;around&lt;/em&gt; the store rather than into it. That is not the only option on offer: hash-chained provenance logs with a verifier you can call ship in paid audit tiers and in open-source memory layers too - cognee's is &lt;a href="https://github.com/topoteretes/cognee/blob/v1.5.3/cognee/modules/provenance/manager.py" rel="noopener noreferrer"&gt;Apache-2.0 and one &lt;code&gt;pip install&lt;/code&gt; away&lt;/a&gt;. Two questions for your own store, then: what does it record about a write that you can read back later, and what does a result tell you about why it ranked where it did. Worth knowing the answers before you build that layer outside it.&lt;/p&gt;

&lt;p&gt;Engrava keeps two things in the box. The &lt;strong&gt;CognitiveJournal&lt;/strong&gt; is a SHA-256 hash-linked log of every mutation - what changed, when, which service wrote it, replayable, and tamper-evident in the sense a hash chain gives you: an edit somewhere in the middle breaks the chain, provided you have a record of the head to check the chain against. It is not a signature. The service name in an entry is whatever the writing process declared it to be, and the chain does not prove who produced it. And hybrid search hands back part of its own working: vector, keyword (BM25) and recency are fused into one score, and the result carries that score per hit plus the set of backends that were available for the query. Be precise about what that is. It tells you which backends were available for the query and how strongly a hit scored overall. It does not tell you which of them returned that hit or what each contributed to its score, and it is not a claim that the ranking is better - better is a benchmark question, and I'm keeping those for when there are numbers to stand behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually useful here
&lt;/h2&gt;

&lt;p&gt;The useful property isn't that Engrava prevents every bad memory. It doesn't. The useful property is that bad memory stays &lt;em&gt;inspectable&lt;/em&gt;: when it was written, what changed, which service name the writing process put on it, and whether the journal still verifies. When something does go wrong, you can open the store and see what happened instead of guessing at a model's mood.&lt;/p&gt;

&lt;p&gt;Engrava is a deterministic, auditable, local-first store, and it does not fix all five of these. The first is kept out of Engrava's own write path, though Engrava will still store bad metadata your code hands it. The second and third get a data model you can act on, not detection. The fourth is contained, not solved. The fifth is half done: the journal is complete, the ranking explanation isn't. Whatever store you end up on, that is the list worth checking it against.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pip install engrava&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The code and the architecture behind it: &lt;a href="https://github.com/sovantica/engrava" rel="noopener noreferrer"&gt;github.com/sovantica/engrava&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>memory</category>
      <category>python</category>
    </item>
    <item>
      <title>A LongMemEval-S number you can reproduce</title>
      <dc:creator>Przemek Marzec</dc:creator>
      <pubDate>Thu, 27 Aug 2026 21:07:28 +0000</pubDate>
      <link>https://dev.to/sovantica/a-longmemeval-s-number-you-can-reproduce-2l0n</link>
      <guid>https://dev.to/sovantica/a-longmemeval-s-number-you-can-reproduce-2l0n</guid>
      <description>&lt;p&gt;We held off on posting a benchmark for a long time. Not because we didn't have runs - because most memory benchmarks you read are a number with no way to check it. A blog says "X%", and you have no idea what reader answered the questions, what judge scored them, how much context the retriever was allowed to feed, or whether an LLM quietly did the hard part inside the "memory" layer. So the number tells you almost nothing about the memory system.&lt;/p&gt;

&lt;p&gt;Here is one we're comfortable standing behind, because you can run it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;On &lt;strong&gt;LongMemEval-S&lt;/strong&gt;, the full 500-question set, &lt;strong&gt;Engrava 0.6.0 scored 81.6% micro in August 2026&lt;/strong&gt; - 81.76% averaged across the six question categories. The run uses the &lt;strong&gt;canonical LongMemEval scorer&lt;/strong&gt; (pinned to a known upstream commit), the standard &lt;code&gt;gpt-4o-2024-08-06&lt;/code&gt; reader and judge over the OpenAI API, and a &lt;code&gt;top_k&lt;/code&gt; of 20 retrieved turns. Nothing about the reader, the prompt, or the scorer is ours; the only thing we swapped in is the memory.&lt;/p&gt;

&lt;p&gt;It is compared against the previous release: &lt;strong&gt;0.5.0, run in July 2026, scored 82.4% micro / 82.58% macro&lt;/strong&gt; on the same 500 questions, same reader, same judge, same scorer, same &lt;code&gt;top_k&lt;/code&gt;. Both rows are on the leaderboard, both &lt;code&gt;verified&lt;/code&gt;, and both ship their reproduction artifacts. We are leading with 0.6.0 because that is the version this post is about; the older row stays because removing it when the number goes down is exactly the move that makes benchmark pages worthless.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;0.5.0 (2026-07-10)&lt;/th&gt;
&lt;th&gt;0.6.0 (2026-08-11)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;micro&lt;/td&gt;
&lt;td&gt;82.4%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;81.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macro&lt;/td&gt;
&lt;td&gt;82.58%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;81.76%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both figures are dated on purpose. This post is a record of two specific runs, not a running scoreboard; the current table, whatever version is newest when you read this, lives on the &lt;a href="https://engrava.ai/benchmarks/" rel="noopener noreferrer"&gt;Engrava benchmarks page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The run also has &lt;strong&gt;no LLM in the memory pipeline.&lt;/strong&gt; Ingestion and retrieval are deterministic - hybrid search over a typed graph, no model doing extraction, summarization, or re-ranking behind the curtain. In the benchmark's own terms this is a &lt;strong&gt;Group A&lt;/strong&gt; run: &lt;code&gt;memory_pipeline_llms: []&lt;/code&gt;. So whatever the score reflects, it is not a second language model inside the memory layer doing part of the work - and not one you'd have to pay for on every write.&lt;/p&gt;

&lt;p&gt;That last part is a cost property, not just an architectural one. A memory layer that calls a generative model on every write - to decide what to store, to summarize it, to re-rank it on read - pays for that model on every operation, so the bill tracks how much the agent reads and writes, not how much it has stored. Engrava's ingest and retrieval are deterministic, so writing and reading memory doesn't spend generative-LLM tokens. It isn't free of model calls entirely - vector search needs an embedding at write time - but that's a cheap, pluggable embedder you can run fully local, not a generative model doing the expensive work on every operation.&lt;/p&gt;

&lt;p&gt;So it's a &lt;strong&gt;retrieval-quality&lt;/strong&gt; result in the sense that retrieval is the only part we swapped: the measurement runs end to end through a fixed reader and a fixed judge, and holding those constant is what makes two runs comparable. It does not make the score ours alone.&lt;/p&gt;

&lt;p&gt;The gap between the two rows is four questions out of five hundred. The next section is about what we can and cannot say about those four.&lt;/p&gt;

&lt;h2&gt;
  
  
  About those four questions
&lt;/h2&gt;

&lt;p&gt;The obvious question about two runs four questions apart is whether the newer version got worse. We went and looked at both runs' artifacts rather than guessing. The artifacts support neither "it regressed" nor "it's just noise":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Between the two runs, engrava handed the reader identical context on 457 of the 500 questions, and on the 43 where the retrieved context differed at all, not one answer changed. Every one of the 32 questions whose outcome moved - 18 down, 14 up, netting the four-question difference - received byte-identical retrieved context in both runs, same passages in the same order. Those flips therefore cannot be attributed to the memory layer: with the input to the reader unchanged, what varied was the reader and judge themselves, which are not deterministic even at temperature zero. We are not claiming the two versions are equivalent, and we have not run a replicate of this configuration, so we have no measured variance for the score itself and will not invent a confidence interval. What we can say precisely is narrower and stronger: this difference is not something engrava's retrieval did.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things that paragraph deliberately does not say, and we won't say them either. It does not call the difference noise - that would be a claim about measurement variance, and measuring that needs replicate runs we have not paid for. And the churn underneath the four-question net - 32 individual outcomes moving, 6.4% of the set - is a measurement from these two runs. It is not an estimate of how much a score wobbles between runs, and we won't present it as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the number comes from
&lt;/h2&gt;

&lt;p&gt;Engrava isn't a vector index with a graph bolted on. The pieces that move a score like this are the same ones in the free package: a &lt;strong&gt;typed knowledge graph&lt;/strong&gt; (thoughts as nodes, seven edge types between them), &lt;strong&gt;hybrid search&lt;/strong&gt; that fuses vector similarity, BM25 over the text, and recency in one query, and turn-level granularity so the retriever can land on the exact user turn a question depends on rather than a blurry session average.&lt;/p&gt;

&lt;p&gt;Those live alongside the rest of what Engrava ships, in one embedded SQLite store. The benchmark exercises the retrieval slice and nothing else. What it shows is that the retrieval half of a local, no-server memory layer holds up on a public long-horizon test.&lt;/p&gt;

&lt;p&gt;The number is &lt;strong&gt;not&lt;/strong&gt; a result about consolidation. Engrava's background consolidation ("dreaming") is deterministic memory hygiene, and it is switched off for this run - so whatever it does or doesn't do for long-horizon recall, this figure does not measure it and we are not reaching for it here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The run lives in a public repo - &lt;a href="https://github.com/sovantica/engrava-benchmark" rel="noopener noreferrer"&gt;&lt;code&gt;sovantica/engrava-benchmark&lt;/code&gt;&lt;/a&gt;, MIT. It isn't a package you install; it's a repo you clone and run against the public &lt;code&gt;engrava&lt;/code&gt; on PyPI. The machine-readable &lt;code&gt;leaderboard.json&lt;/code&gt; in that repo is the number of record; this post just describes two of its rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/sovantica/engrava-benchmark.git
&lt;span class="nb"&gt;cd &lt;/span&gt;engrava-benchmark
git checkout a45dde9                 &lt;span class="c"&gt;# the runner commit this result pins&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
make &lt;span class="nb"&gt;install
&lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"engrava==0.6.0"&lt;/span&gt;        &lt;span class="c"&gt;# the exact version the result pins&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...            &lt;span class="c"&gt;# reader + judge, OpenAI-direct&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ENGRAVA_BENCH_LONGMEMEVAL_S&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;path&amp;gt;/longmemeval_s_cleaned.json  &lt;span class="c"&gt;# the cleaned split (see note below)&lt;/span&gt;

python runners/longmemeval/run.py    &lt;span class="c"&gt;# no flags - the bare command is the canonical run&lt;/span&gt;
make validate
make leaderboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bare command with no flags &lt;strong&gt;is&lt;/strong&gt; the canonical configuration - the same reader, judge, scorer, and &lt;code&gt;top_k&lt;/code&gt; every published number uses. Any flag that overrides a model or endpoint moves the row out of the comparable segment, so it's no longer the headline. If you want to check the wiring before spending anything on API calls, &lt;code&gt;python runners/longmemeval/run.py --smoke&lt;/code&gt; runs real Engrava retrieval against a local embedder and a mock reader/judge for free.&lt;/p&gt;

&lt;p&gt;Get the dataset right: it is the authors' &lt;strong&gt;cleaned&lt;/strong&gt; LongMemEval-S release, from Hugging Face &lt;a href="https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned" rel="noopener noreferrer"&gt;&lt;code&gt;xiaowu0162/longmemeval-cleaned&lt;/code&gt;&lt;/a&gt; (&lt;code&gt;longmemeval_s_cleaned.json&lt;/code&gt;) - not the raw &lt;code&gt;longmemeval_s.json&lt;/code&gt;. The result row pins that dataset by sha256, so before you run, confirm your file's hash matches the row's &lt;code&gt;dataset_revision&lt;/code&gt;; a different revision produces a different score.&lt;/p&gt;

&lt;p&gt;Every result row pins the axes that move a score - engrava version and distribution hash, runner commit, reader and judge snapshots and endpoints, scorer version, retriever, granularity, &lt;code&gt;top_k&lt;/code&gt; - plus a reproduction artifact and its checksum.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;p&gt;The two rows sit in separate comparability segments, and exactly one axis separates them: the harness commit. Dataset revision by hash, reader and judge snapshots, scorer commit, retriever and &lt;code&gt;top_k&lt;/code&gt; are identical. That is why they are worth putting side by side, and why the board still keeps them apart - a number is comparable only within a segment matching on all of those axes, harness included, and different segments are never merged into one ranked table. Both sit in the canonical &lt;code&gt;gpt-4o-2024-08-06&lt;/code&gt; reader and judge; a score produced with a different reader isn't rank-comparable to either, and we don't present it that way. This is one retrieval benchmark on one dataset - a real signal about long-horizon recall, not a universal statement about every workload. And it's the retrieval slice specifically: the structural guarantees Engrava also ships (the hash-linked journal, typed edges, MindQL) are their own thing, verifiable in their own right, and not what this figure measures.&lt;/p&gt;

&lt;p&gt;If you're evaluating agent memory, don't take the percentage on faith - run the command above, then swap in whatever else you're weighing by writing one adapter, and read both numbers off the same reader and scorer. That's the comparison that actually tells you something.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;engrava
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/sovantica/engrava" rel="noopener noreferrer"&gt;github.com/sovantica/engrava&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Benchmark: &lt;a href="https://github.com/sovantica/engrava-benchmark" rel="noopener noreferrer"&gt;github.com/sovantica/engrava-benchmark&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>benchmark</category>
      <category>python</category>
    </item>
    <item>
      <title>What our coverage number did not protect</title>
      <dc:creator>Przemek Marzec</dc:creator>
      <pubDate>Mon, 24 Aug 2026 20:20:02 +0000</pubDate>
      <link>https://dev.to/sovantica/what-our-coverage-number-did-not-protect-2ck2</link>
      <guid>https://dev.to/sovantica/what-our-coverage-number-did-not-protect-2ck2</guid>
      <description>&lt;p&gt;Engrava's test suite had 3,845 tests and 94.22% line coverage. There was more test code in the repository than production code. By every number we had, the thing was well tested.&lt;/p&gt;

&lt;p&gt;Then we ran a mutation audit over it and found that the suite did not protect a broken concurrency guard, several deletion paths, five configuration sections, two of its own safety tests, or a field that would silently discard a legitimate value, in the version people were running. That is nine findings in three groups, and the groups are not alike. The accounting is below.&lt;/p&gt;

&lt;p&gt;This post is about how that happens, because the mechanism is more interesting than the individual bugs, and because we would have told you the suite was solid the day before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage answers a question nobody asked
&lt;/h2&gt;

&lt;p&gt;Line coverage tells you a line executed while the tests ran. It does not tell you that anything would have noticed if the line were wrong.&lt;/p&gt;

&lt;p&gt;Those sound close. They are not. A test can execute a guard, assert something true about the result, and stay green after you delete the guard entirely, because the outcome it asserts is produced by something else in the path. The line was covered the whole time. It was never checked.&lt;/p&gt;

&lt;p&gt;The instrument for finding this is mutation testing: change the production code deliberately, run the suite, and see whether anything goes red. What survives the mutation is the finding. Not the code you broke, the tests that let you.&lt;/p&gt;

&lt;p&gt;We ran that against Engrava before the 0.6.0 release. Here is what it returned, grouped honestly, because the three groups are not the same kind of thing and lumping them together would flatter us in one direction and alarm you in another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Group one: six guards we hardened, none reachable from outside the process
&lt;/h2&gt;

&lt;p&gt;Six findings were guards that validated a value and then used the caller's original object rather than the validated one. Validate, then discard the result of validating.&lt;/p&gt;

&lt;p&gt;If you have ever written a validator, you know this shape immediately. You check an argument, the check passes, and then the code below reaches for the argument again instead of for what the check produced. If the value can answer differently between those two reads, the check and the use are no longer talking about the same thing.&lt;/p&gt;

&lt;p&gt;Six of those were reachable (and here is the part that matters more than the finding) &lt;strong&gt;only by a caller already executing code inside the same process.&lt;/strong&gt; Not through a config file, not through a query string, not through data arriving from anywhere. Every one needed code running next to ours, deliberately handing the library something built to behave inconsistently.&lt;/p&gt;

&lt;p&gt;We checked that against the published release itself rather than against our own descriptions of what we had fixed, which is a different claim. The answer was the same for all six: &lt;strong&gt;none of them crossed a trust boundary in any version that was ever on PyPI.&lt;/strong&gt; No advisory is owed, no CVE, no patch to the 0.5 line.&lt;/p&gt;

&lt;p&gt;The framing we ended up with, and it is the honest one: &lt;strong&gt;this is the absence of a boundary, not a hole in one.&lt;/strong&gt; Someone executing arbitrary code in your process can replace the function these guards protect, or the standard library call underneath it. Defending against that specific caller is not a thing a library can do, and a test asserting we had would be theatre.&lt;/p&gt;

&lt;p&gt;So why harden them at all? Because defence in depth is worth having when it is cheap, because the mechanism generalizes to places where the caller &lt;em&gt;is&lt;/em&gt; less trusted, and because a guard that can be talked out of its own conclusion is wrong on its own terms even when nothing can currently exploit it. We fixed all six. We are not going to describe them in language that suggests you were exposed, because you were not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Group two: two safety tests that proved nothing
&lt;/h2&gt;

&lt;p&gt;This is the group that unsettled us most.&lt;/p&gt;

&lt;p&gt;Two tests in the hygiene suite (the code path that decides what memory gets archived and eventually deleted) passed over guards that were working correctly. They were not detecting the guards. They were detecting something else in the path that happened to produce the same outcome.&lt;/p&gt;

&lt;p&gt;One of them had a docstring stating, in plain English, that a specific pin was what caused the row to be skipped. That was not true. A different condition entirely was doing the skipping, and you could remove the pin protection at all three layers and watch the test stay green.&lt;/p&gt;

&lt;p&gt;Nobody wrote those tests carelessly. They were written by people who understood the feature, and read by people who understood it too. Reading a test tells you what its author believed. It does not tell you what the test can detect. Only mutating the code it claims to protect tells you that, and neither of us thought to do that until a sweep did it mechanically.&lt;/p&gt;

&lt;p&gt;There is a related finding that makes the point sharper. Elsewhere in the codebase, deleting a guarantee outright left &lt;strong&gt;4,316 tests green&lt;/strong&gt;: the whole suite, not some narrow slice of it. A green suite is not evidence that a guarantee holds. It is evidence that nothing in the suite noticed it was gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Group three: the one that could actually lose your data
&lt;/h2&gt;

&lt;p&gt;Nothing above changed what a normal user's data did: the first group needs code already running inside your process, and the second is about our tests rather than about shipped behaviour. This one is different, and it is the reason this post exists in the form it does.&lt;/p&gt;

&lt;p&gt;Engrava's edges carry a &lt;code&gt;decay_multiplier&lt;/code&gt;, a float that is documented as valid from &lt;code&gt;0.0&lt;/code&gt; upward. Setting it to &lt;code&gt;0.0&lt;/code&gt; is a legitimate thing to do: it means this edge does not decay by that mechanism.&lt;/p&gt;

&lt;p&gt;In released 0.5.x, the code that read that value back from the database tested it for truthiness. &lt;code&gt;0.0&lt;/code&gt; is falsy. So a stored &lt;code&gt;0.0&lt;/code&gt; read back as &lt;code&gt;1.0&lt;/code&gt;, the default.&lt;/p&gt;

&lt;p&gt;That alone would be a bad-but-recoverable read bug. It got worse on the way out: the update path rebuilds the whole edge record from the values it just read and writes every column back. So the next time anything updates that edge (for an unrelated reason: changing the weight, touching metadata), the misread &lt;code&gt;1.0&lt;/code&gt; is written over your &lt;code&gt;0.0&lt;/code&gt;. Permanently. No error, no warning, no log line.&lt;/p&gt;

&lt;p&gt;No attacker. No unusual configuration. Ordinary correct use of the public, documented API, silently discarding a value the documentation says is valid.&lt;/p&gt;

&lt;p&gt;It is fixed in 0.6.0. &lt;strong&gt;If you set &lt;code&gt;decay_multiplier=0.0&lt;/code&gt; on any 0.5.x release, check those edges, and note that upgrading does not restore a value that was already overwritten.&lt;/strong&gt; The 0.5 database has no record of what the number used to be.&lt;/p&gt;

&lt;p&gt;We are stating this plainly because a post about our own testing gap that quietly omitted the one defect that could cost someone data would be worth less than not writing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the count actually is
&lt;/h2&gt;

&lt;p&gt;Six defence-in-depth hardenings, all of which require code already running in the process. Two vacuous tests over working guards. One real data-loss defect.&lt;/p&gt;

&lt;p&gt;Those nine are the same nine the opening lists: the concurrency guard, the deletion paths and the configuration sections are among the six; the hygiene safety tests are the two; the field that could lose data is the one. We are spelling the arithmetic out because the easy version of this post describes all nine in security language, and that would be a worse error than any of the individual findings: six of them cannot be reached by anyone who is not already running code in your process, and two of them are about our tests rather than about anything that shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed, and what we did not
&lt;/h2&gt;

&lt;p&gt;The suite went from 3,845 tests to 4,316; coverage went from 94.22% to about 95%. The coverage move is the least interesting number on this page, which is the argument closing on itself.&lt;/p&gt;

&lt;p&gt;The changes that matter are structural. Guards now use the value the validation produced. Several hand-maintained lists that were supposed to enumerate protected operations are now derived from the code rather than typed out and hoped over. The two vacuous tests are able to fail. And we now require, for a claim about a test, that someone demonstrate the signal going red on the broken version, not that the suite is green on the fixed one.&lt;/p&gt;

&lt;p&gt;What we did not do: replace the concurrency mechanism. The specific guard the audit broke is fixed, but the broader design question underneath it (how two processes writing to one file coordinate) needs a schema change, and landing that in a release days from publishing was the wrong trade. It is deferred and written down as deferred, rather than rushed in so this post could have a tidier ending.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try this on your own suite
&lt;/h2&gt;

&lt;p&gt;You do not need a mutation-testing framework to start. Take a guard you believe is tested: an authorization check, a validation, a deletion safety condition. Delete it. Run your tests.&lt;/p&gt;

&lt;p&gt;If they stay green, you have learned something about your suite that no coverage report was ever going to tell you. That is the whole technique. The tooling only makes it systematic.&lt;/p&gt;

&lt;p&gt;We found six hardenings, two tests that proved nothing, and one real bug this way, in a codebase we would have described the day before as well tested. We think that says less about Engrava than about what a coverage number is capable of promising.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;engrava
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/sovantica/engrava" rel="noopener noreferrer"&gt;github.com/sovantica/engrava&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Upgrade notes: &lt;a href="https://github.com/sovantica/engrava/blob/v0.6.0/docs/upgrade.md" rel="noopener noreferrer"&gt;github.com/sovantica/engrava/blob/v0.6.0/docs/upgrade.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>python</category>
      <category>softwareengineering</category>
      <category>ai</category>
    </item>
    <item>
      <title>Engrava 0.5.0: a first-class MCP server</title>
      <dc:creator>Przemek Marzec</dc:creator>
      <pubDate>Sat, 22 Aug 2026 20:36:42 +0000</pubDate>
      <link>https://dev.to/sovantica/engrava-050-a-first-class-mcp-server-2djc</link>
      <guid>https://dev.to/sovantica/engrava-050-a-first-class-mcp-server-2djc</guid>
      <description>&lt;p&gt;Engrava 0.5.0 pulls the MCP server out of the library.&lt;/p&gt;

&lt;p&gt;Install &lt;code&gt;engrava&lt;/code&gt; now and you get the memory library and nothing else. The server ships separately, as &lt;code&gt;engrava-mcp&lt;/code&gt;. The two were never really the same kind of thing — one is a dependency you import, the other is a process your MCP client spawns — and keeping them in one package meant everyone who installed the library also pulled in the server's dependencies, whether or not they ran a server. Splitting them gives each a clean install surface.&lt;/p&gt;

&lt;p&gt;If you build directly on the Python API, nothing about your install changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;engrava
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want Engrava as a memory server for an MCP client, reach for the standalone package instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx engrava-mcp
&lt;span class="c"&gt;# or&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;engrava-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;engrava-mcp&lt;/code&gt; is a native stdio Model Context Protocol server that sits in front of an Engrava store. It exposes read tools, optional write tools, &lt;code&gt;engrava://&lt;/code&gt; resources, and guided prompts to any MCP client that speaks stdio.&lt;/p&gt;

&lt;p&gt;It's also listed in the official MCP Registry as &lt;code&gt;ai.sovantica/engrava&lt;/code&gt; — PyPI package &lt;code&gt;engrava-mcp&lt;/code&gt;, stdio transport — so a client that reads the registry can find it without you wiring anything by hand.&lt;/p&gt;

&lt;p&gt;One detail that looks like a mismatch and isn't: the library is &lt;code&gt;engrava&lt;/code&gt; 0.5.0, but the server you install is &lt;code&gt;engrava-mcp&lt;/code&gt; 0.5.1. The two version independently. &lt;code&gt;engrava-mcp&lt;/code&gt; follows Engrava's minor line rather than its patch releases — every 0.5.x build targets &lt;code&gt;engrava&amp;gt;=0.5,&amp;lt;0.6&lt;/code&gt; — so it moves on its own patch schedule underneath. Today that lands at 0.5.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed for MCP users
&lt;/h2&gt;

&lt;p&gt;In 0.4 the server rode along inside &lt;code&gt;engrava&lt;/code&gt; as the &lt;code&gt;engrava[mcp]&lt;/code&gt; extra. In 0.5 it graduates to a package of its own:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pip install "engrava[mcp]"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pip install engrava-mcp&lt;/code&gt; or &lt;code&gt;uvx engrava-mcp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;engrava-mcp&lt;/code&gt; installed by &lt;code&gt;engrava&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;engrava-mcp&lt;/code&gt; installed by the standalone package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP client command: &lt;code&gt;engrava-mcp&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MCP client command: &lt;code&gt;uvx&lt;/code&gt;, args: &lt;code&gt;["engrava-mcp"]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This only breaks you if you were running Engrava as an MCP server. A plain &lt;code&gt;pip install engrava&lt;/code&gt; library upgrade is untouched, and the server still reads the same &lt;code&gt;engrava.yaml&lt;/code&gt; or database path it always did — what moves is the install command and the launch command your client uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else shipped in 0.5.0
&lt;/h2&gt;

&lt;p&gt;The packaging is the visible change, but the library moved too, mostly around retrieval and the audit path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scoped ranked retrieval&lt;/strong&gt; — metadata and visibility filters now apply inside the ranked retrieval path, not only the raw scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit verification&lt;/strong&gt; — check the hash-chain journal from code with &lt;code&gt;store.verify_journal()&lt;/code&gt;, or from a shell with &lt;code&gt;engrava --db engrava.db verify&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed provenance capture&lt;/strong&gt; — callers can attach bounded write-time context such as &lt;code&gt;session_id&lt;/code&gt; and &lt;code&gt;actor_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability fixes&lt;/strong&gt; — the sqlite-vec backend and several hybrid-fusion edge cases behave correctly now. If you were hitting either, this is the release that fixes it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a set of opt-in lifecycle features you can switch on and evaluate against your own workload: consolidation activation, deterministic memory hygiene, and caller-side whole-turn assembly. They ship in 0.5.0, off by default; whether they earn a place in your setup is yours to judge.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;engrava&lt;/code&gt; is the library, &lt;code&gt;engrava-mcp&lt;/code&gt; is the server — install whichever you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;engrava
uvx engrava-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code and server docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sovantica/engrava" rel="noopener noreferrer"&gt;Engrava&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sovantica/engrava-mcp" rel="noopener noreferrer"&gt;Engrava MCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/engrava-mcp/" rel="noopener noreferrer"&gt;PyPI: engrava-mcp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>vectordatabase</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Action Records: Memory for Things an Agent Did</title>
      <dc:creator>Przemek Marzec</dc:creator>
      <pubDate>Mon, 20 Jul 2026 20:26:10 +0000</pubDate>
      <link>https://dev.to/sovantica/action-records-memory-for-things-an-agent-did-4472</link>
      <guid>https://dev.to/sovantica/action-records-memory-for-things-an-agent-did-4472</guid>
      <description>&lt;p&gt;Most agent memory examples start with facts: a user preference, a summary, a document chunk, a note extracted from a conversation. Those are useful, but they leave out a large part of what an agent actually needs to reason about later: what it planned to do, what it attempted, what succeeded, and what failed.&lt;/p&gt;

&lt;p&gt;Engrava models that surface with &lt;strong&gt;Action Records&lt;/strong&gt;. An action isn't a chat message, and it isn't a replacement for a task runner. It's a durable memory record for a state change the agent considers operationally meaningful.&lt;/p&gt;

&lt;p&gt;That distinction stays abstract until an agent has to recover context across sessions. "The deployment was discussed" is a different thing to have in memory than "the deployment was attempted and failed after the migration step." A text summary can hold the second sentence, but a first-class action record gives the agent something structured to query, connect, and verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why actions belong in memory
&lt;/h2&gt;

&lt;p&gt;Agents move through a loop: infer what should happen next, call a tool or ask a human for approval, observe the result, adjust. If memory only stores natural-language notes, that loop goes opaque over time — the agent can search for words, but it can't reliably ask for "the last failed attempt to update this resource" or "actions that were planned but never confirmed."&lt;/p&gt;

&lt;p&gt;Action Records make that state explicit. They represent states like &lt;code&gt;PLANNED&lt;/code&gt;, &lt;code&gt;EXECUTING&lt;/code&gt;, &lt;code&gt;CONFIRMED&lt;/code&gt;, and &lt;code&gt;FAILED&lt;/code&gt;, and connect to thoughts and edges in the same local store. This isn't about turning the memory system into an orchestrator — it's about keeping the memory database honest about the gap between what the agent knows and what it actually did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured state, still embedded
&lt;/h2&gt;

&lt;p&gt;Engrava keeps this in the same embedded SQLite-backed store as the rest of the memory graph, which matters for a couple of reasons. There's no separate service to stand up just to ask operational questions — thoughts, edges, action records, timestamps, lifecycle state, and optional journal entries all live in one local database. And the action state participates in the same query and retrieval model: an agent can record a thought for a request, link it to a planned action, update that action when the tool call completes, then later use MindQL and the Python API to inspect recent actions, narrow by status, or pull back the surrounding graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  The journal is evidence, not a spell
&lt;/h2&gt;

&lt;p&gt;When journaling is enabled, Engrava records thought and edge mutations and action state transitions as hash-linked journal entries — a tamper-evident chain for the events that were journaled. The wording is deliberate. The journal is optional. It is not a database-wide integrity system, it doesn't make external side effects reversible, and it doesn't stop a privileged writer from replacing the database and journal together. What it gives you is a way to verify the continuity of the journaled chain when that journal has been enabled — which matters because action state has a different trust profile from an ordinary note. If an agent later sees an action marked confirmed, the application may want to know whether that confirmation is part of the expected local history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not just a task log
&lt;/h2&gt;

&lt;p&gt;A task log answers "what happened?" Agent memory has to answer a wider question: not only that an action happened, but what the agent believed when it chose it, which earlier facts led there, whether it moved through a known state sequence, and whether a failure should change how it retrieves or plans next time. Action Records are built to live &lt;em&gt;with&lt;/em&gt; the surrounding graph, not beside it — so execution memory stays close to semantic memory instead of collapsing into unstructured prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

&lt;p&gt;Say an agent is rolling out a config change. It stores a thought for the request, records a &lt;code&gt;PLANNED&lt;/code&gt; action linked to that thought, and moves the action to &lt;code&gt;EXECUTING&lt;/code&gt; when it calls the deploy tool. The migration step fails, so the action lands in &lt;code&gt;FAILED&lt;/code&gt; — not as a line buried in a summary, but as a queryable record tied to the resource and to the thoughts around it.&lt;/p&gt;

&lt;p&gt;Two sessions later the agent picks the task back up. Instead of re-reading a wall of notes hoping the failure is mentioned, it asks the store directly: the last &lt;code&gt;FAILED&lt;/code&gt; action on that resource, and the thoughts connected to it. It retrieves the failed migration and the reasoning linked to it — and can choose not to repeat it blindly.&lt;/p&gt;

&lt;p&gt;A plain text log could hold "deploy failed." What it can't do is let the agent ask that question precisely, connect the answer to the decision that led there, and treat a failed action differently from a fact it merely read. That's the point of giving actions a first-class record instead of leaving them in prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it sits
&lt;/h2&gt;

&lt;p&gt;Action Records are one piece of a compositional store: graph memory for relationships, hybrid search for recall, MindQL for structured reads, and an optional tamper-evident journal. None of it claims the memory "thinks" for the agent — the application still owns policy, approvals, and side effects. Engrava just gives it a local, queryable substrate that can remember more than facts alone.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>python</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How we built Engrava: from cognitive-architecture research to a production library</title>
      <dc:creator>Przemek Marzec</dc:creator>
      <pubDate>Tue, 14 Jul 2026 20:23:39 +0000</pubDate>
      <link>https://dev.to/sovantica/how-we-built-engrava-from-cognitive-architecture-research-to-a-production-library-4i7i</link>
      <guid>https://dev.to/sovantica/how-we-built-engrava-from-cognitive-architecture-research-to-a-production-library-4i7i</guid>
      <description>&lt;p&gt;Deterministic consolidation, a typed graph in SQLite, and an honest look at what agent-memory benchmarks can and can't measure.&lt;/p&gt;

&lt;p&gt;You're building an agent. It answers questions across many sessions, and by session three it has forgotten what it learned in session one.&lt;/p&gt;

&lt;p&gt;The reflex is to put a vector database in front of it - and now it remembers a blurry average of everything, ranked by cosine distance, contradicting itself and unable to tell a three-week-old preference from a stale throwaway. A graph database gives you structure, at the cost of a second persistence model with its own query language and deployment. A managed memory service starts you in a few lines, and moves the decision of what your agent remembers onto someone else's infrastructure.&lt;/p&gt;

&lt;p&gt;Underneath all of them is one problem: if every plausible fact is written the moment it appears, memory turns into an accumulation layer instead of a judgement layer. Engrava is our answer to that - local, structured, and deliberate about what it keeps in reach. Here is how it is built, and why.&lt;/p&gt;

&lt;h3&gt;
  
  
  We started with a question, not a&amp;nbsp;schema
&lt;/h3&gt;

&lt;p&gt;Before the storage design there was a long stretch of reading cognitive-architecture research, circling one question: what does a long-running agent actually need from memory if that memory has to stay inspectable? That framing is the reason Engrava is a typed graph and not a bag of embeddings, the reason consolidation is deterministic instead of an LLM rewrite pass, and the reason extraction stays above the database instead of hiding inside it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the memory actually&amp;nbsp;is
&lt;/h3&gt;

&lt;p&gt;Engrava is a typed knowledge graph with hybrid search, in a single SQLite file. Thoughts are nodes; typed edges carry the relationships a flat vector can't - that A caused B, that C specializes D. Retrieval fuses vector similarity, keyword match, recency, and priority rather than leaning on cosine distance alone. Turn on the journal and every thought and edge mutation is recorded in a tamper-evident SHA-256 chain. It is all in the free package.&lt;/p&gt;

&lt;h3&gt;
  
  
  How consolidation works
&lt;/h3&gt;

&lt;p&gt;Every thought has a priority, recomputed each time the consolidation cycle (the dreaming: block) runs - one deterministic pass over the store weighing five signals: recency, staleness, confirmation, confidence, frequency. They combine into a score, checked against gates before promotion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dreaming&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;signals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;recency&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;staleness&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;confirmation&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;confidence&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;frequency&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;promote_threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75&lt;/span&gt;
  &lt;span class="na"&gt;gates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;min_confirmations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;max_promoted_per_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default is conservative on purpose: with min_confirmations: 2, a fact the agent has seen once is not promoted - it stays fully retrievable, just not lifted into the active set until the agent has re-confirmed it. That is the point of the cycle: keep memory a judgement layer, so a single noisy pass can't reshape what the agent treats as settled. No language-model call, no network call, no embedding recomputation - arithmetic over SQLite rows, same inputs and same outputs every run, and the policy is a YAML file you can review in a pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it is shaped like the brain's memory - and where it&amp;nbsp;isn't
&lt;/h3&gt;

&lt;p&gt;Three findings shaped three choices. Diekelmann &amp;amp; Born (2010) describe memory stabilization during sleep as selective - the brain keeps a subset of traces, not all of them, which is what the priority score and the promotion gates do. Yassa &amp;amp; Stark (2011) describe how the hippocampus keeps similar experiences from collapsing into one average, so in Engrava thoughts stay distinct nodes and similarity doesn't auto-merge them. Rao &amp;amp; Ballard (1999), and Clark's synthesis (2013), frame the brain as stabilizing what repeated experience confirms, which is where the confirmation and confidence signals come from. We did not build a brain - we took the pressures biological memory evolved under and applied them to a file on disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  The part of benchmarks nobody wants to&amp;nbsp;say
&lt;/h3&gt;

&lt;p&gt;We didn't tune Engrava to top a leaderboard. And dreaming - the consolidation cycle that gives Engrava its character - doesn't move our retrieval benchmark. We turned it on, we turned it off, and the accuracy barely shifts.&lt;/p&gt;

&lt;p&gt;It isn't a bug we're hiding; it's a sign the benchmark is measuring something dreaming isn't for. These benchmarks are short-horizon question-answering over a fixed transcript. Dreaming is lifecycle management - over the weeks an agent stays alive, it decides what stays in reach and what settles out, so the store doesn't rot into an accumulation layer. Whether that discipline pays off over long horizons is genuinely hard to measure, and the benchmarks that exist can't see it.&lt;/p&gt;

&lt;p&gt;Comparable evidence across consolidation systems is thin, too: implementations, datasets, reader models, and evaluation setups vary too much for clean comparisons, and the underlying sleep-consolidation research is itself contested. So we won't attach an improvement claim to dreaming that our own retrieval test doesn't support. Dreaming is deterministic, inspectable, and does a specific job; whether that job matters is yours to decide.&lt;/p&gt;

&lt;p&gt;The wider point holds for the field: half of these benchmarks aren't measuring the same mechanism. Some run a language model inside the memory pipeline; some don't. And the reader model can move the headline number more than the memory architecture does - hold the memory fixed, swap in a stronger reader, and the same system posts a very different score. When we do publish a number, we publish the whole run - reader, judge, dataset, setup - so anyone can reproduce it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stealing SQLite's&amp;nbsp;posture
&lt;/h3&gt;

&lt;p&gt;SQLite runs inside the host process, writes to one file, ships as one library, and is one of the most-tested databases in existence. We wanted that posture. Engrava is a Python library - pip install engrava, and the store is a file on your disk. No server to run, no port to open, no separate auth; your agent imports it like any dependency, nothing leaves the host unless you wire up a remote embedding provider yourself, and it is MIT-licensed. It is not a multi-tenant fleet service - if your agent is a horizontally-scaled cluster needing shared memory across machines, embedded is the wrong shape. For a single agent, it is usually the simpler fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the category is&amp;nbsp;now
&lt;/h3&gt;

&lt;p&gt;When we started, this was fairly open space. By the time we shipped it wasn't - several teams, Anthropic among them, had shipped consolidation under the same "dreaming" name, borrowing the same sleep metaphor, around the same time. The category converged fast. What separates these tools now isn't the vocabulary; it's whether the consolidation is something you can open up, configure, and run yourself, or something that happens elsewhere on your behalf.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's next
&lt;/h3&gt;

&lt;p&gt;Engrava is live on &lt;a href="https://pypi.org/project/engrava/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; - the graph, deterministic consolidation, hybrid search, the audit journal, and MindQL are all in the free package. &lt;br&gt;
If you're building an agent, try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;engrava
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo is on &lt;a href="https://github.com/sovantica/engrava" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;; issues and discussions are open.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>agents</category>
      <category>memory</category>
    </item>
  </channel>
</rss>
