{"slug": "why-your-ai-agent-s-memory-says-it-saved-but-didn-t", "title": "Why your AI agent's memory says it saved but didn't", "summary": "A developer documented a state-assertion bug in agentmemory, an open-source memory system for coding agents, in which the MCP shim wrote saves to a local fallback file (~/.agentmemory/standalone.json) and returned a valid memory ID while the server never received the data, leaving saves unrecoverable for roughly six days while the engine was hung. An open patch adds a storage: \"local\" field and a warning that the server did not confirm the call, making the failure legible rather than preventing it. A second report showed POST /agentmemory/import with strategy \"replace\" deleting existing scopes before validating optional sections.", "body_md": "On October 13, a user of agentmemory, an open-source memory system for coding agents, filed an issue with a code block that should not exist. Two `memory_save` calls, two returned IDs, and about six days later, no memories. The engine had hung on October 7. It held its listening sockets open, never accepted a connection, and the client kept confirming saves that never reached disk. Call it the state-assertion bug: a system's report about its own state diverges from the state.\n\nIt is not a retrieval failure. We think it is the most expensive failure class in agent memory, because it corrupts the one asset you need to recover from every other failure, the record of what happened, and it does so one green check at a time.\n\nThis post takes three threads of that class from agentmemory and pairs them with the four escalating fixes we ran on the same class inside our own memory discipline, over two days in October. The short version: a status claim is not evidence. Make the claim name the check that would prove it, make the check read the live system rather than the record of it, and make the wrong thing impossible before you make it visible.\n\nThe issue is titled \"hung agentmemory was not saving but giving ids,\" and the body is nearly boring, which is what makes it useful. The reporter ran two saves and pasted the responses:\n\n```\n{\"saved\":\"mem_mv6tghgk_02a5a2f023b8\"}\n{\"saved\":\"mem_mv6tghzw_4aba68f9c6f7\"}\n```\n\nA later session could not find either one. The reporter's diagnosis: the engine had been hung for about six days, holding its listeners but never accepting, so the UI showed zero memories while both saves returned IDs for rows that never touched disk.\n\nThe follow-up pull request explains the mechanism better than the report does. When the health check or a proxied call fails, the MCP shim writes the memory to a local fallback file, `~/.agentmemory/standalone.json`, and replies `{\"saved\": id}`. Nothing copies that file to the server. The fallback is announced only on stderr.\n\nThat last sentence is the whole story. The save did happen. The receipt was accurate about the ID and silent about the address, and the address is the only part that matters, because a memory in a file the server never reads is a memory you will never recall.\n\nThe open patch does not prevent the loss. It prevents the lie: fallback saves and deletes would carry `storage: \"local\"` and a warning that the server did not confirm the call. A patch that makes a failure legible is worth more than a patch that makes it rare, because the rare one still happens on a day you are not looking.\n\nThat pull request also shows the second form of the same bug: a labeling problem wearing a storage costume. The user's mental state is \"it is not saving.\" The system's state is \"it is saving somewhere the server does not read.\" Both sentences are true, and the interface offers one word for three different outcomes: saved, queued, and gone.\n\nThis shape is everywhere: an offline write queue, a mock returning fixtures, a cache serving stale values. The write path is not wrong. The vocabulary is.\n\nThe test we now apply to any status field is whether it can be false. A field that always reads `saved` is decoration, not a status. If the server did not confirm, the response should say the server did not confirm, and it should name the store that actually holds the bytes. The patch here does exactly that, and the interesting part is how small it is. One field and one warning, in the place where the user is already looking.\n\nContrast that with the original signal. The fallback was announced on stderr, which is where truthful information goes to be ignored. A warning that requires a human to tail a log during an agent loop is a note in a drawer.\n\n`success: true` after the data is gone\nOn October 16, a second report took the same shape through a different door. `POST /agentmemory/import` with `strategy: \"replace\"` deletes every existing scope before it checks the optional sections. Send `lessons` as an empty object, and the response is `success: true` with every lesson gone. Send it as a string, and the call throws after the deletes. Either way the store is empty by the time anyone finds out.\n\nThe proposed fix validates before deleting. That is the strongest shape in this post, because it does not depend on anyone reading a warning at the right moment. A malformed section is rejected before anything is lost.\n\nWhy do tests miss this? Because each step is correct alone. Replace does delete scopes. The validator does reject malformed sections. The handler does return success. The bug is the order, and order rarely gets its own test until someone has been burned by it.\n\nRetrieval errors announce themselves. You ask for a fact, get the wrong one, and notice. A state-report error waits. You build the next three steps on it, and when something finally breaks, you debug using a record that is already false.\n\nThen comes the calibration cost. Every time the interface says fine and it isn't, you pay twice: once for the lost write, and once for the habit of not checking. That habit is the expensive part, and nothing in a dashboard measures it.\n\nThe benchmark layer does not cover this. agentmemory publishes 95.2% R@5 on LongMemEval-S from its own run, reproducible from the harness in the repo. That number is real, and it says nothing about whether a write reached the store. Retrieval quality and write truthfulness are different contracts. A system can be excellent at the first and still fail the second on a Tuesday, which is roughly what happened here.\n\nNone of this is an argument against the project. The tracker is public, the fix proposals are public, and the reports are specific enough to reproduce. The interesting fact is that a team this careful about evaluation still shipped the failure. Carelessness is the wrong explanation. The cause is a status message that promises more than the system can know.\n\nWe recognized the shape because we have been losing to it for months. Our own memory discipline makes claims about a live project: what shipped, what is still in flight, what was decided. Four times in two days in October, one of those claims was wrong, and each fix was a stronger shape than the last.\n\nThe first fix added a question to our instructions. Before stating a status, ask whether this session actually observed it. Within a day we produced two wrong status sentences, one built on a snapshot that was twenty-five days old and one that called an item pending after an exception had already closed it. A question in a document is a reminder, and reminders are read by the same mind that is about to be wrong.\n\nA script replaced the question the next day. It reads five sources in a fixed priority order, verdicts first and stale plans last, and prints which source says what. It also added a write-back rule: when an item closes, the plan line gets a pointer. The script was correct. What failed was the installation. We recorded that the check ran before status sentences, and the file that would have told anyone to run it never contained that line. The gate existed in the report about the gate.\n\nThen came a machine light. A daily scan flags log entries that contain status words without an evidence anchor. That scan found real problems, and it still missed the failure that mattered, because it scanned the log while the failure happened in a sentence about to be spoken in a conversation. The sensor was mounted in the archive while the leak was in the room.\n\nThe fourth fix moved the check into the generator. A live mode reads the draft's front matter, the built output, and the live page before a status claim can be written, and the drafting template has a field for the evidence that cannot be left blank. The class recurred afterward in a sentence rather than a document. We count that as evidence for the ranking below, not against the check, because a check only covers the surface it is mounted on.\n\n| Shape | When it acts | What it gives you | Example | \n|---|---|---|---|\n| Make it impossible | Before the action | The bad state cannot occur | Validate before delete (#1576) | \n| Wire it to the moment | At the action | The reader sees the truth at the moment of choice | Response says `storage: \"local\"` (#1563) | \n| Detect after the fact | After the action | You can find it later, if you look | Daily scan for unevidenced claims | \n| Remember to remember | Depends on the moment | Nothing you can rely on | Our self-check fixes: 0 for 4 | \n\nRank the shapes by where the failure can enter, not by how much effort the fix takes. The weakest shape is also the one that feels most like progress, because writing a rule feels like doing something, and the day you write it is usually the day the last failure is still fresh.\n\nAcross our four rounds, the first three rules were all true. The question was right, the script was right, the scan was right. They failed because they lived in a channel the decision maker does not visit at the moment of decision. That is the same defect as a fallback announced on stderr, and it is why we now rank fixes by when they act.\n\nOur rule now has two halves. A positive claim names the check that would prove it, and that check reads the live system, not the record of the live system. A negative claim, the kind that says not done, does not exist, or cannot be done, has to answer three questions before it leaves the desk:\n\nThese questions are mechanical on purpose. They are the cheapest substitute we have for a machine that cannot be lied to, and they work best when the answer has to name a command and its output.\n\nFour things to copy on Monday:\n\n`memory_save` returns an ID. Recall that ID in a fresh session and assert the content, not the status code. One extra call buys you the only evidence that counts.\nOur failure registry is still event driven and written by a human, which means the judgment stays human and the coverage depends on someone noticing. The live check reads the post source, the build output, and the live URL. It does not read the rest of what we claim about our own work. The patches here are mostly reactive in the same way: the fallback still writes to a file the server never reads, and the open patch makes it say so.\n\nThere is a specification question underneath, and we would like to see it taken seriously. When a memory API cannot confirm that a write reached durable storage, should the response say `saved` at all? Accepted, queued, and confirmed are three different promises, and most memory APIs collapse them into one. The first vendor to publish that distinction in its contract will make this entire bug class cheaper to avoid, for everyone, including the systems that already handle it well.\n\nUntil then, the discipline is the same one we apply to our own records: treat every status message as a claim, and check the claim against the live system before you build on it. The memory files we use are open source in [wallaby-agent-rules](https://github.com/Dawncoral/wallaby-agent-rules), and the failure registry behind this post is internal, which is the next asymmetry worth closing.\n\nDisclosure: Wallaby Token sells inference for open-weight models, and we keep our own project records in a file-based memory discipline. We have a stake in this layer, so check the receipts and the links.", "url": "https://wpnews.pro/news/why-your-ai-agent-s-memory-says-it-saved-but-didn-t", "canonical_source": "https://dev.to/dawncoral/why-your-ai-agents-memory-says-it-saved-but-didnt-34d8", "published_at": "2026-10-19 15:32:37+00:00", "updated_at": "2026-10-19 15:44:50.914402+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-safety", "developer-tools"], "entities": ["agentmemory", "MCP"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/why-your-ai-agent-s-memory-says-it-saved-but-didn-t", "markdown": "https://wpnews.pro/news/why-your-ai-agent-s-memory-says-it-saved-but-didn-t.md", "text": "https://wpnews.pro/news/why-your-ai-agent-s-memory-says-it-saved-but-didn-t.txt", "jsonld": "https://wpnews.pro/news/why-your-ai-agent-s-memory-says-it-saved-but-didn-t.jsonld"}}