A customer loaded 26,633 facts into Echo Memory. Every call returned a well formed response. None of them carried an error, isError was false throughout, and the ingest script counted 26,633 successes.
1,650 facts were stored.
Nothing failed. That is the part worth sitting with, because it is the part that generalises past this one system: a write API that answers can be wrong in a way no amount of error handling on the caller’s side will catch.
Three ways to return without storing
write_episode takes entities and the facts connecting them. It could finish with nothing written in three different ways, and from the caller’s side they were indistinguishable.
It refused. Validation rejected the episode. This one at least carried an error field, so a caller checking for errors noticed.
It deferred. An entity name was similar enough to an existing node to be worth asking about, and not similar enough to merge. Rather than guess, the server returned the candidates and skipped those facts, waiting for the caller to say which entity it meant. This is the right behaviour and it is the one that cost the 25,000 facts, because a deferral is not an error: no error field, no exception, a perfectly ordinary response with an empty edges_created.
Everything already existed. The episode asserted facts the store already held, word for word, so there was nothing new to write.
A caller could distinguish them, in principle, by inspecting a different key for each case. Nobody does that. Everybody checks for an error, gets none, and counts a success.
The same bug from the other end
A different customer wrote 543 episodes and ended up with 112 facts. Not because anything was deferred, but because the writes were duplicates and duplicates were being stored as if they were changes.
Edge identity in the graph is (source, target, relation_type). It never included the fact text, and the code that created an edge ran unconditionally. So writing the same sentence twice created a second edge, ended the first, and emitted a supersession whose before_fact was identical to its after_fact. One customer edge accumulated 47 versions of a single sentence, each one dutifully audited as a change to the record.
Reproduced before fixing it: three identical writes, three versions, one distinct text.
Why the obvious fix is the wrong one
The tempting fix is to skip a write when the fact text matches. That is wrong, and the reason is the interesting part of this whole problem.
A fact is not only its sentence. It carries a confidence, which says whether the claim was extracted from something stated or inferred, and a causal_hint, which is what makes the fact traceable at all. Retyping a claim from inferred to extracted is a different claim about how it is known. Adding a causal hint to an untyped fact is the edit that makes a chain assemble.
So the hard part was never detecting a duplicate. It was not detecting a change as one. Most of the tests written for this are about what still has to supersede: changed text, changed confidence, a newly typed hint.
What the write answers now
Every write reports stored, a boolean, and when it is false it names which of the four cases it was: refused, deferred, unchanged, or no_facts. A refusal carries the same keys as a success, so one check works on every path. A retryable flag separates a database blip worth retrying from a validation error that will fail forever. A write that stored some facts and held the rest says partial.
The restatement case stores nothing and reports the fact it matched. That also makes retries safe, which they were not before: with no idempotency key, a caller retrying a write that had actually landed used to add an edge and burn a supersession on it.
The general shape
Two of these four cases are correct behaviour. Deferring on an ambiguous entity is better than guessing which customer a name refers to, and skipping an unchanged fact is better than versioning it. The defect was never the behaviour. It was that the response could not tell you which of them happened.
If you are building a write API where some inputs are legitimately no-ops, the test is not whether the caller can find out what happened. It is whether a caller who does the obvious thing finds out. The obvious thing is checking for an error, and a correct no-op is not an error.
One boolean would have saved 25,000 facts.
Echo Memory is a memory graph for AI agents that never calls a model to write. The write path described here is in the repository, Apache 2.0, along with the tests that pin each of these cases. More on the data model in why a graph, not a vector store.