<?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: qianqiuwanzi</title>
    <description>The latest articles on DEV Community by qianqiuwanzi (@qianqiuwanzi).</description>
    <link>https://dev.to/qianqiuwanzi</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%2Fuser%2Fprofile_image%2F4127114%2Fa8e08787-440e-4370-93e8-c8f9c64ad35c.jpg</url>
      <title>DEV Community: qianqiuwanzi</title>
      <link>https://dev.to/qianqiuwanzi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/qianqiuwanzi"/>
    <language>en</language>
    <item>
      <title>Testing an agent memory layer: the assertions that actually catch decay</title>
      <dc:creator>qianqiuwanzi</dc:creator>
      <pubDate>Wed, 16 Sep 2026 06:28:57 +0000</pubDate>
      <link>https://dev.to/qianqiuwanzi/testing-an-agent-memory-layer-the-assertions-that-actually-catch-decay-4g8e</link>
      <guid>https://dev.to/qianqiuwanzi/testing-an-agent-memory-layer-the-assertions-that-actually-catch-decay-4g8e</guid>
      <description>&lt;p&gt;A memory layer is the one component in an agent stack where the tests can all pass while the system quietly rots.&lt;/p&gt;

&lt;p&gt;That's not a testing-discipline problem. It's a shape problem. Every other part of the stack is a function: input in, output out, assert equality. A memory store is a &lt;em&gt;relationship graph over time&lt;/em&gt;. Its failures aren't wrong values — they're stale values, ambiguous values, and values that used to be true. None of those show up in an assertion like &lt;code&gt;expect(recall(q)).toEqual([...])&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here are the six assertions I ended up automating, roughly in order of how early they catch something.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Supersession integrity
&lt;/h2&gt;

&lt;p&gt;If entries can supersede one another, the graph has to be well-formed. Three checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reciprocal links: if &lt;code&gt;A.superseded_by = B&lt;/code&gt;, then &lt;code&gt;B.supersedes&lt;/code&gt; contains &lt;code&gt;A&lt;/code&gt;. A one-directional link means your history is unreadable in one of the two directions.&lt;/li&gt;
&lt;li&gt;No cycles. &lt;code&gt;A → B → C → A&lt;/code&gt; is always a bug, and it's the kind of bug that makes recall hang or return an arbitrary member of the cycle.&lt;/li&gt;
&lt;li&gt;No orphan supersession: &lt;code&gt;superseded_by&lt;/code&gt; pointing at an id that was hard-deleted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This one runs in milliseconds and has caught more real bugs for me than anything else on the list. It's a pure graph invariant, so it needs no fixture and no judgement.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Staleness ratio
&lt;/h2&gt;

&lt;p&gt;Of the entries your recall path actually returns over a sample of real queries, what fraction are already marked superseded?&lt;/p&gt;

&lt;p&gt;This is the closest thing to a single health number I've found. It should be near zero, and its &lt;em&gt;trend&lt;/em&gt; matters more than its value. A store where this climbs from 2% to 15% over a month is telling you consolidation isn't running, or isn't looking at the right population.&lt;/p&gt;

&lt;p&gt;The assertion isn't a magic threshold — it's a budget. Pick a number you're willing to defend, fail the test when you exceed it, and treat the failure as "your consolidation job is broken," not "the test is too strict."&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Recall determinism
&lt;/h2&gt;

&lt;p&gt;Same store state, same query, same result set — including the ordering.&lt;/p&gt;

&lt;p&gt;This sounds trivial and it isn't, because ties happen constantly: embedding scores cluster, timestamps collide, and whatever sort your store uses is not guaranteed stable. Non-deterministic recall produces the worst class of bug: an agent that behaves differently on Tuesday for no visible reason, which you then misattribute to the model.&lt;/p&gt;

&lt;p&gt;If you genuinely need variety, make it explicit and seeded. Don't let it fall out of an unstable sort.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Write-time dedupe
&lt;/h2&gt;

&lt;p&gt;Write the same assertion twice with different wording, then assert that the store holds one canonical entry — not two — with both sources attached.&lt;/p&gt;

&lt;p&gt;The interesting failure isn't the obvious duplicate. It's the near-duplicate that shouldn't be merged: "we use Postgres" versus "we use Postgres for the billing service only." A good test corpus contains both cases, and the assertion is that the first merges and the second does not.&lt;/p&gt;

&lt;p&gt;That pair of tests is the only thing standing between you and an over-eager merge threshold that quietly averages two distinct decisions into one useless entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Conflict surfacing
&lt;/h2&gt;

&lt;p&gt;Inject a deliberately contradictory pair, then assert that the recall path &lt;em&gt;flags&lt;/em&gt; the conflict rather than silently picking one.&lt;/p&gt;

&lt;p&gt;This is a behaviour test, not a data test, and it's the one most projects skip. An agent that confidently reads a stale decision is strictly worse than an agent that says "I have two conflicting notes, which is current?" The first one is fast and wrong; the second one costs you four seconds and a question.&lt;/p&gt;

&lt;p&gt;Assert the flag exists. Assert the conflicting pair is named in the output. Don't assert the resolution, because resolution is domain-specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Decay monotonicity
&lt;/h2&gt;

&lt;p&gt;All else equal, an entry that hasn't been referenced in six months must not outrank a fresh entry of the same type and comparable relevance.&lt;/p&gt;

&lt;p&gt;All else is never actually equal, so test it as a controlled pair: two synthetic entries, identical type and text shape, different ages, identical access counts. The older one must not win. If it does, your decay function is decorative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the corpus
&lt;/h2&gt;

&lt;p&gt;Assertions are cheap; the fixture is the work. Two sources, in order of value:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Golden pairs from real sessions.&lt;/strong&gt; Take twenty real queries you've actually run, and write down the memory entry a good recall would return. This is a slow, manual job and it's worth it — twenty honest pairs beat two hundred generated ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synthetic invariants.&lt;/strong&gt; The graph checks, the dedupe pair, the conflict pair, the decay pair. These need no domain knowledge and can be written in an afternoon.&lt;/p&gt;

&lt;p&gt;Then run everything against a &lt;strong&gt;seeded fixture store&lt;/strong&gt;, never production. Memory tests that read your real store are tests you'll disable within a week, because they'll fail for reasons that have nothing to do with the code you changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a failure actually means
&lt;/h2&gt;

&lt;p&gt;Map each assertion to a cause, or the test suite becomes noise:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assertion fails&lt;/th&gt;
&lt;th&gt;Almost always means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supersession integrity&lt;/td&gt;
&lt;td&gt;A write path that bypassed the link bookkeeping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staleness ratio&lt;/td&gt;
&lt;td&gt;Consolidation isn't running, or isn't scoped to all entry types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall determinism&lt;/td&gt;
&lt;td&gt;Unstable sort, or a tie broken by insertion order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write-time dedupe&lt;/td&gt;
&lt;td&gt;Merge threshold too loose (merged) or too tight (didn't)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflict surfacing&lt;/td&gt;
&lt;td&gt;Recall returns text without checking validity windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decay monotonicity&lt;/td&gt;
&lt;td&gt;Decay weight computed but not applied to ranking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six assertions, one afternoon to write, and the failure table is the actual deliverable — because the point isn't that the tests go green. It's that when a green suite turns red, you know which subsystem moved.&lt;/p&gt;




&lt;p&gt;This is part of a series on building a local-first memory layer for coding agents. Part 1 covered the failure modes getting memory to work at all; part 2 covered consolidation — the stage that keeps a store from rotting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/qianqiuwanzi/i-gave-my-ai-coding-agents-a-local-long-term-memory-layer-8-things-that-broke-a5i"&gt;I gave my AI coding agents a local long-term memory layer — 8 things that broke&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/qianqiuwanzi/consolidation-the-half-of-agent-memory-nobody-builds-4lfg"&gt;Consolidation: the half of agent memory nobody builds&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see how the four access paths (MCP, desktop, Python/Node SDK) are wired: &lt;a href="https://hm.qianshi.cool/api/v2/dl?from=devto" rel="noopener noreferrer"&gt;https://hm.qianshi.cool/api/v2/dl?from=devto&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Consolidation: the half of agent memory nobody builds</title>
      <dc:creator>qianqiuwanzi</dc:creator>
      <pubDate>Wed, 16 Sep 2026 03:59:07 +0000</pubDate>
      <link>https://dev.to/qianqiuwanzi/consolidation-the-half-of-agent-memory-nobody-builds-4lfg</link>
      <guid>https://dev.to/qianqiuwanzi/consolidation-the-half-of-agent-memory-nobody-builds-4lfg</guid>
      <description>&lt;p&gt;Every agent memory project I've seen (including the first two versions of mine) builds two things: &lt;code&gt;record&lt;/code&gt; and &lt;code&gt;recall&lt;/code&gt;. You write something down, you read it back. Ship it.&lt;/p&gt;

&lt;p&gt;Then, three weeks in, recall quality drops — and nobody can explain why, because nothing in the system got worse. Nothing in the system got &lt;em&gt;cleaned&lt;/em&gt;, either.&lt;/p&gt;

&lt;p&gt;That's the missing stage. Consolidation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs4lt23cnmeawm2pozisl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs4lt23cnmeawm2pozisl.png" alt="The five-stage memory lifecycle, with consolidation highlighted as the stage that is usually missing" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just store everything" degrades
&lt;/h2&gt;

&lt;p&gt;Three mechanisms, all of them boring and all of them certain:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicates.&lt;/strong&gt; The same fact gets recorded five times across five sessions, each with slightly different wording, because nothing at write time knew it already existed. Retrieval now returns five near-identical entries and burns its budget on one fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent contradiction.&lt;/strong&gt; The store says &lt;code&gt;using poetry for dependency management&lt;/code&gt; and &lt;code&gt;switched to uv because CI install time&lt;/code&gt; — both true, one superseded. Nothing marks which one is current. The agent picks whichever one scores higher on embedding similarity that day. You get non-deterministic behaviour that looks like a model problem and is actually a data problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unbounded growth.&lt;/strong&gt; If nothing is ever pruned or compacted, a fixed retrieval budget has to cover an ever-larger candidate set. Recall doesn't fail loudly; it just gradually returns more generic, less useful context.&lt;/p&gt;

&lt;p&gt;None of these are fixed by a better embedding model. They're fixed by a process that runs on the store itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four operations
&lt;/h2&gt;

&lt;p&gt;Consolidation is not one thing. It's four, and they have different failure modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Merge
&lt;/h3&gt;

&lt;p&gt;Two entries that assert the same thing should become one, with both sources attached. The hard part is not detecting similarity — it's deciding when similarity means &lt;em&gt;same assertion&lt;/em&gt; versus &lt;em&gt;related but distinct&lt;/em&gt;. "We use Postgres" and "we use Postgres in the billing service only" are 0.9 similar and must not be merged.&lt;/p&gt;

&lt;p&gt;Practical rule I use: merge automatically on high-confidence matches, and queue anything in the ambiguous band for review. Do not let the ambiguous band be auto-merged. That band is exactly where the information lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Supersede — never delete
&lt;/h3&gt;

&lt;p&gt;When a decision is reversed, the old entry should remain, marked as superseded, linked to the entry that replaced it. Deleting it is the tempting option and it's the wrong one, because the reason a decision was reversed is the thing you'll need again in three months.&lt;/p&gt;

&lt;p&gt;An entry needs, at minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mem_7f3a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"use uv instead of poetry for dependency resolution"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"poetry's lockfile resolution was too slow in monorepo CI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-11T09:12:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid_until"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"supersedes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mem_2c91"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"superseded_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"session/2026-04-11#turn-42"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;supersedes&lt;/code&gt; / &lt;code&gt;superseded_by&lt;/code&gt; turn your memory store into a graph rather than a bag. It's what lets you answer "why is it like this now" instead of only "what is it now".&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Decay
&lt;/h3&gt;

&lt;p&gt;Old entries shouldn't be deleted; they should lose rank. Store a decay weight derived from age and access, and let retrieval use it as one signal among several. An entry that hasn't been relevant in six months but is still true should be cheap to keep and cheap to ignore.&lt;/p&gt;

&lt;p&gt;Deletion is for garbage (duplicated, malformed, explicitly retracted). Everything else ages.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Conflict check
&lt;/h3&gt;

&lt;p&gt;Two checks, at two different times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;At write time&lt;/strong&gt;: does this new entry contradict something already stored? If yes, don't refuse the write — mark the pair and resolve it in the next consolidation pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At recall time&lt;/strong&gt;: if two returned entries conflict, surface that. An agent that confidently reads a stale decision is worse than an agent that says "I have two conflicting notes here."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Design details that actually matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Type your entries.&lt;/strong&gt; Free-text blobs retrieve badly. &lt;code&gt;fact&lt;/code&gt; / &lt;code&gt;decision&lt;/code&gt; / &lt;code&gt;pitfall&lt;/code&gt; retrieve under different policies, and mixing them means you always get an average of everything. "This project uses uv" (fact), "we chose uv because CI needed to be fast" (decision), and "poetry's lockfile has a path bug in monorepos" (pitfall) need different ranking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make consolidation idempotent and reviewable.&lt;/strong&gt; It should produce a diff you can read, not an opaque rewrite. If a background process silently rewrites your project history and you can't audit it, you've built a system you can't trust — and you won't notice until it has already misled you a dozen times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it out of band.&lt;/strong&gt; Not in the request path. Consolidation is a batch job that runs between sessions; doing it inline adds latency to the exact moment you're waiting on the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anchor memories to something stable.&lt;/strong&gt; Bind entries to a git remote or a project id, not an absolute path. Directory renames and moves are common and will otherwise silently detach the entire history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things I got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Over-merging first.&lt;/strong&gt; My earliest merge threshold was too permissive and it collapsed distinct decisions into one averaged entry. Recovering the original two required going back to raw session logs. The information was gone from the store and only existed in the transcript. Aggressive merging feels like progress and quietly destroys the thing you built the system for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating consolidation as a one-shot cleanup.&lt;/strong&gt; I ran it once, was happy with the result, and made it manual. It needs to be scheduled. The store doesn't stay tidy; it's a garden, not a build artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell it's working
&lt;/h2&gt;

&lt;p&gt;I deliberately won't quote numbers here, because my deployment is small and any percentage I gave you would be noise dressed as evidence. What I do track are proxies that are meaningful even at small scale:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Count of superseded entries&lt;/td&gt;
&lt;td&gt;Whether reversals are being captured at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Longest supersession chain&lt;/td&gt;
&lt;td&gt;Whether you can reconstruct decision history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share of recall results that are already superseded&lt;/td&gt;
&lt;td&gt;Your staleness rate — should trend toward zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate clusters merged per pass&lt;/td&gt;
&lt;td&gt;Whether write-time filtering is too loose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size of the ambiguous review queue&lt;/td&gt;
&lt;td&gt;Whether you're guessing too much&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the staleness share is climbing, your consolidation isn't running or isn't looking at the right thing. That ratio is the closest thing to a health check I've found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this sits in my setup
&lt;/h2&gt;

&lt;p&gt;I run this as a local-first memory layer — everything on local disk, retrieval local, no cloud round-trip, and consolidation as a scheduled offline pass. The tradeoffs are real and I'll state them plainly: no cross-device sync (you move the files yourself), no team-shared memory, and it's Windows-only right now. macOS is planned, not shipped.&lt;/p&gt;

&lt;p&gt;Access is over MCP, a desktop connection, or Python / Node SDKs depending on the client. And one honest caveat for anyone outside China: the trial signup uses a Chinese phone number or WeChat login, so it's realistically aimed at a China-based audience — I'd rather say that up front than have you hit a wall at the signup screen.&lt;/p&gt;

&lt;p&gt;If you're building memory for an agent and you've reached the point where storing works but recall is degrading, consolidation is probably the missing piece. The two questions worth asking yourself: &lt;em&gt;can I reconstruct why a decision changed?&lt;/em&gt; and &lt;em&gt;do I know what fraction of what I hand the model is stale?&lt;/em&gt; If either answer is no, that's the next thing to build.&lt;/p&gt;

&lt;p&gt;I write about this as I go — part 1 covered the eight failure modes I hit getting memory to work at all:&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://dev.to/qianqiuwanzi/i-gave-my-ai-coding-agents-a-local-long-term-memory-layer-8-things-that-broke-a5i"&gt;I gave my AI coding agents a local long-term memory layer — 8 things that broke&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you want to look at the implementation: &lt;a href="https://hm.qianshi.cool/api/v2/dl?from=devto" rel="noopener noreferrer"&gt;https://hm.qianshi.cool/api/v2/dl?from=devto&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I gave my AI coding agents a local long-term memory layer — 8 things that broke</title>
      <dc:creator>qianqiuwanzi</dc:creator>
      <pubDate>Wed, 16 Sep 2026 02:53:20 +0000</pubDate>
      <link>https://dev.to/qianqiuwanzi/i-gave-my-ai-coding-agents-a-local-long-term-memory-layer-8-things-that-broke-a5i</link>
      <guid>https://dev.to/qianqiuwanzi/i-gave-my-ai-coding-agents-a-local-long-term-memory-layer-8-things-that-broke-a5i</guid>
      <description>&lt;p&gt;Every new chat window starts from zero.&lt;/p&gt;

&lt;p&gt;I've been running coding agents (Claude Code, Cursor, Codex, and a rotating cast of others) long enough that this stopped being a minor annoyance and became the actual bottleneck. The agent isn't bad at the task. It's bad at remembering why the task looks the way it does — that we moved off approach A last Tuesday, that the flaky test is flaky for a known reason, that the config flag exists because of a specific incident.&lt;/p&gt;

&lt;p&gt;So I spent a while trying to fix it. Then I spent longer fixing the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tried first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Project rule files (&lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;.cursorrules&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Works for static facts. Fails for anything that changes. The moment a decision gets revised, the rule file is stale and now actively misleading — and nothing tells you it's stale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pasting context manually&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reliable, until you forget. And you will forget, on the day you're moving fastest. Also: pasting 2000 tokens of background means those tokens are now competing with your actual question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A very long system prompt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It grows. Then you start maintaining it like a codebase, except there's no test suite, so you can't tell what's still load-bearing. I removed a paragraph I was sure was useless and watched output quality drop.&lt;/p&gt;

&lt;p&gt;The pattern in all three: I was asking the model — or the client — to be the memory. Neither of them is built for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach that actually held
&lt;/h2&gt;

&lt;p&gt;Stop trying to make the model remember. Give it something to &lt;em&gt;query&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Memory goes to disk, outside the context window. The agent gets tools to read and write it. Four responsibilities, kept separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;record&lt;/strong&gt; — accept a memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;recall&lt;/strong&gt; — retrieve the relevant ones for the current task&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;consolidation&lt;/strong&gt; — merge, deduplicate, and resolve conflicts over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;file-bridge&lt;/strong&gt; — pull project files in as raw material&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping those four apart was the single most useful architectural decision, because every failure below lands squarely in one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8 things that broke
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The MCP server's lifetime is not yours&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The MCP layer is a thin proxy; the actual memory service is a separate long-running process. If it isn't running, tool calls fail — and the error surfaces at the moment the agent needs memory most, which is exactly the wrong time to discover it. Treat the transport process as stateless and disposable, keep all state on disk, and make "service not running" a clear, actionable message rather than a stack trace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "Remember everything" is worse than remembering nothing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first version stored indiscriminately and dumped the top N results into context. Output got &lt;em&gt;worse&lt;/em&gt;. Irrelevant memories don't just waste tokens — they actively mislead. Recall quality is a ranking problem, and ranking needs a relevance signal, not a recency sort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Two agents writing at once&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I run more than one agent. When two wrote to the same store concurrently, I got interleaved and occasionally contradictory records. Fixed with a single-writer discipline plus locking. Boring, unglamorous, non-optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Memories go stale and start contradicting each other&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After a few weeks: "we use Postgres" and "we migrated off Postgres." Both true at their own timestamp, both useless to an agent that can't tell which is current. You need an explicit consolidation pass that detects conflict and supersedes — not just dedup by string similarity. This is the part I underestimated most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Recall latency is on the critical path&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reading and ranking from disk on every turn adds visible delay. Cheap fix: maintain an index and impose a hard time budget on recall. If it can't rank in the budget, return fewer results rather than making the user wait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Anything that reads project files will read &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a component ingests project files, it will eventually ingest a secret. Filter at the ingestion boundary, before it's written — not at read time, and not "later." Retrofitting a filter onto a store that already contains credentials is a genuinely bad afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Deciding what's worth ingesting is harder than reading it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The file-bridge is trivial to &lt;em&gt;build&lt;/em&gt; and hard to &lt;em&gt;tune&lt;/em&gt;. Reading a directory is easy. Knowing which files carry durable context — and which are generated noise — is the actual product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Evaluation is the thing nobody budgets for&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How do you know recall improved? I couldn't answer this for weeks, which meant "improvements" were vibes. What worked: keep a replay set of real past tasks and check whether the right memories surface for each. Without it, you're tuning blind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs I accepted on purpose
&lt;/h2&gt;

&lt;p&gt;These are decisions, not oversights, so I'll state them plainly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;What it costs you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No cloud sync — memory is written to local disk only&lt;/td&gt;
&lt;td&gt;Moving machines is manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval runs locally, no cloud calls&lt;/td&gt;
&lt;td&gt;No team-shared memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows only right now — macOS is planned for a closed beta in Q1 2027&lt;/td&gt;
&lt;td&gt;Bad news today if you're on macOS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last one I'd change if I could. I can't yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is, if you want to try it
&lt;/h2&gt;

&lt;p&gt;The product is &lt;strong&gt;HyperMarrow&lt;/strong&gt;. It's a local long-term memory layer for coding agents — the four modules above, with four ways to connect (MCP server, direct desktop integration, Python SDK, Node SDK). It runs on Windows.&lt;/p&gt;

&lt;p&gt;There's a 30-day full-feature trial, with no email/password to set up — sign-in is by WeChat QR or SMS code. I'll be straight with you, because you'll find out anyway: that also means it currently expects a Chinese phone number, so if you're outside China the download is mostly useless to you. The eight failure modes above are the transferable part.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://hm.qianshi.cool/api/v2/dl?from=devto" rel="noopener noreferrer"&gt;https://hm.qianshi.cool/api/v2/dl?from=devto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If a different approach works better for you, the eight failure modes above are the useful part of this post — they're the same whether you build it yourself or adopt something. Steal the checklist.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
