<?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: Statewave</title>
    <description>The latest articles on DEV Community by Statewave (@statewave).</description>
    <link>https://dev.to/statewave</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%2F4060888%2Fe34a17f1-1419-4fc2-998a-cf9c07700a48.png</url>
      <title>DEV Community: Statewave</title>
      <link>https://dev.to/statewave</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/statewave"/>
    <language>en</language>
    <item>
      <title>Statewave Guide — Day 4: We passed the safety test. Then failed the product test</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Thu, 15 Oct 2026 20:53:57 +0000</pubDate>
      <link>https://dev.to/statewave/statewave-guide-day-4-we-passed-the-safety-test-then-failed-the-product-test-a40</link>
      <guid>https://dev.to/statewave/statewave-guide-day-4-we-passed-the-safety-test-then-failed-the-product-test-a40</guid>
      <description>&lt;p&gt;&lt;em&gt;Built 27 Aug 2026 · written up 14 Oct 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yesterday I asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can technically safe product knowledge survive contact with an actual user?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not particularly. 😅&lt;/p&gt;

&lt;p&gt;One model, one fixture, same as yesterday: every number below comes from &lt;code&gt;anthropic-opus-5&lt;/code&gt; against a single application we wrote ourselves, with patterns we knew to look for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wrong-feature claims accepted     0
Unsupported claims accepted       0
Model restraint                   30/30
Verified claim yield              95%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice.&lt;/p&gt;

&lt;p&gt;Then we asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Would any of this actually help a human?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The usefulness scores below were not produced by people. A separate model, OpenAI GPT-5.6 Sol, scored the twenty-one features against a fixed rubric. It is not the model under test, so this is not self-scoring, but it is not a human review either, and no amount of it becomes one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 / 3   median usefulness
24%     rated genuinely useful
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing was dangerously wrong.&lt;/p&gt;

&lt;p&gt;Nothing was completely useless either.&lt;/p&gt;

&lt;p&gt;It was worse in a much more software-engineering kind of way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;almost everything was mediocre.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We assumed the remaining problem would be missing capabilities.&lt;/p&gt;

&lt;p&gt;The data disagreed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.43   features with a verified capability
1.14   features without one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Barely a difference.&lt;/p&gt;

&lt;p&gt;That was how we read it at the time. A later round, with the language problem below fixed, put the same split at 2.00 against 1.07. The capability signal was real; we were reading it through bad presentation.&lt;/p&gt;

&lt;p&gt;Meanwhile:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;14 / 21 outputs were too technical.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thirteen of twenty-one were flagged as missing a capability.&lt;/p&gt;

&lt;p&gt;Then we found the smoking gun.&lt;/p&gt;

&lt;p&gt;Ten features exposed this sentence directly to the user:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"No capability, route or permission has been verified for this feature."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every single one was rated too technical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10/10.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fair enough. 😂&lt;/p&gt;

&lt;p&gt;That's not product copy.&lt;/p&gt;

&lt;p&gt;That's a debugger having an existential crisis in front of the customer.&lt;/p&gt;

&lt;p&gt;Another feature had excellent evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ POST endpoint        ✓ Create permission
✓ Submit path          ✓ Button
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And our deterministic renderer proudly produced:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You can create a new invoices create form and submit the invoices create form form…"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Technically grounded.&lt;/p&gt;

&lt;p&gt;Linguistically deceased.&lt;/p&gt;

&lt;p&gt;That exposed another boundary.&lt;/p&gt;

&lt;p&gt;We had already separated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what the application does   from   what the model thinks it does
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we needed to separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what we know   from   what a human actually needs to hear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the architecture gained another layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product Model        →  what do we know?
Guidance Compiler    →  what is worth telling the user?   ← new
Renderer             →  how should we say it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No verification rules loosened.&lt;/p&gt;

&lt;p&gt;No extra authority for the model.&lt;/p&gt;

&lt;p&gt;We were finally admitting something painfully obvious:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Technically correct data structures do not automatically know how to talk to humans.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Shocking discovery for software developers, I know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statewave Guide — Day 4.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can we turn verified product truth into human guidance without inventing anything new?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This day in the repo: &lt;a href="https://github.com/smaramwbc/statewave-guide/pull/4" rel="noopener noreferrer"&gt;PR #4&lt;/a&gt; · Previous: &lt;a href="https://www.statewave.ai/blog/statewave-guide-day-3" rel="noopener noreferrer"&gt;Day 3 — True is not the same as relevant&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Statewave Guide is built on &lt;strong&gt;Statewave&lt;/strong&gt; — an open-source, self-hosted memory runtime for AI agents. Governance and provenance, not just retrieval. Apache 2.0.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://statewave.ai" rel="noopener noreferrer"&gt;statewave.ai&lt;/a&gt; · &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;github.com/smaramwbc/statewave&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every post in this series: &lt;a href="https://www.statewave.ai/blog/statewave-guide" rel="noopener noreferrer"&gt;The Statewave Guide Journey Index&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ux</category>
      <category>llm</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Statewave Guide — Day 3: True is not the same as relevant</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:55:32 +0000</pubDate>
      <link>https://dev.to/statewave/statewave-guide-day-3-true-is-not-the-same-as-relevant-3me5</link>
      <guid>https://dev.to/statewave/statewave-guide-day-3-true-is-not-the-same-as-relevant-3me5</guid>
      <description>&lt;p&gt;&lt;em&gt;Built 26 Aug 2026 · written up 12 Oct 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yesterday I asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can a model add product meaning to this graph without being allowed to invent product truth?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But apparently factual truth still isn't enough. 😅&lt;/p&gt;

&lt;p&gt;Today we finally put a real model behind Statewave Guide.&lt;/p&gt;

&lt;p&gt;One model, one fixture: every number below comes from &lt;code&gt;anthropic-opus-5&lt;/code&gt; against a single application we wrote ourselves, with patterns we knew to look for. Read it as a measurement of a design change, not of what a model will do on your code.&lt;/p&gt;

&lt;p&gt;The good news first.&lt;/p&gt;

&lt;p&gt;Across hundreds of factual claims under deliberately hostile conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invented routes reaching the Product Model     0
Invented permissions                           0
Unknown subjects                               0
Forbidden capabilities refused                 27/27
Unsupported actions refused                    23/23
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier did its job.&lt;/p&gt;

&lt;p&gt;Then came the less exciting number.&lt;/p&gt;

&lt;p&gt;The model found the right evidence about &lt;strong&gt;89%&lt;/strong&gt; of the time…&lt;/p&gt;

&lt;p&gt;…but only turned it into a verifiable product claim about &lt;strong&gt;20%&lt;/strong&gt; of the time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;89%   found the right evidence
20%   became a verifiable claim
71%   features with no verified capability

workflow recovery: 18%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So technically:&lt;/p&gt;

&lt;p&gt;Very safe.&lt;/p&gt;

&lt;p&gt;Practically:&lt;/p&gt;

&lt;p&gt;A beautifully verified empty room. 😂&lt;/p&gt;

&lt;p&gt;Then we found the interesting bug.&lt;/p&gt;

&lt;p&gt;One claim passed verification that should never have been attached to the feature being analyzed.&lt;/p&gt;

&lt;p&gt;The model said, roughly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Settings changes are saved by submitting the settings form.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Structurally?&lt;/p&gt;

&lt;p&gt;Correct.&lt;/p&gt;

&lt;p&gt;The form exists.&lt;/p&gt;

&lt;p&gt;It submits.&lt;/p&gt;

&lt;p&gt;The relationship exists.&lt;/p&gt;

&lt;p&gt;Every verification rule passed.&lt;/p&gt;

&lt;p&gt;There was only one problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It was true about the application, but not about the feature we were asking about.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The verifier answered:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this statement supported by real evidence?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;What it didn't answer was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Does this evidence actually belong to THIS feature?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a very different class of error.&lt;/p&gt;

&lt;p&gt;And it gave us today's rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Truth is not enough. Context owns truth.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An application graph is a neighborhood.&lt;/p&gt;

&lt;p&gt;Related components, sibling elements, routes and services naturally sit next to each other.&lt;/p&gt;

&lt;p&gt;If an assistant can grab any true fact from that neighborhood, it can produce answers that are technically correct and completely irrelevant.&lt;/p&gt;

&lt;p&gt;So the next layer became &lt;strong&gt;Feature Scope&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A claim now has to prove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the subject exists&lt;/li&gt;
&lt;li&gt;the evidence exists&lt;/li&gt;
&lt;li&gt;the evidence supports the claim&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;the claim belongs to the feature being explained&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also learned that we were asking the model to do too much.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find the evidence.
Choose the claim.
Choose the action.
Choose the subject.
Choose the relationships.
Then explain it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source graph already knows most of that.&lt;/p&gt;

&lt;p&gt;So we changed direction:&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model → invent claim → Verifier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we moved toward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Graph → deterministic claim opportunities → Model adds meaning → Verifier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same safety boundary.&lt;/p&gt;

&lt;p&gt;Less guessing.&lt;/p&gt;

&lt;p&gt;And it worked.&lt;/p&gt;

&lt;p&gt;Verified claim yield went from roughly &lt;strong&gt;20% → 95%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Wrong-feature claims went to &lt;strong&gt;0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Model restraint reached &lt;strong&gt;30/30&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What did not move: &lt;strong&gt;45 of 63&lt;/strong&gt; features still end with no verified capability. Safer, not fuller.&lt;/p&gt;

&lt;p&gt;At that point we had something very safe.&lt;/p&gt;

&lt;p&gt;So naturally we asked the question we should probably have asked earlier:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Does any of this actually help a human?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Statewave Guide — Day 3.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can technically safe product knowledge survive contact with an actual user?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This day in the repo: &lt;a href="https://github.com/smaramwbc/statewave-guide/pull/3" rel="noopener noreferrer"&gt;PR #3&lt;/a&gt; · Previous: &lt;a href="https://www.statewave.ai/blog/statewave-guide-day-2" rel="noopener noreferrer"&gt;Day 2 — Unknown is better than wrong&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Statewave Guide is built on &lt;strong&gt;Statewave&lt;/strong&gt; — an open-source, self-hosted memory runtime for AI agents. Governance and provenance, not just retrieval. Apache 2.0.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://statewave.ai" rel="noopener noreferrer"&gt;statewave.ai&lt;/a&gt; · &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;github.com/smaramwbc/statewave&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every post in this series: &lt;a href="https://www.statewave.ai/blog/statewave-guide" rel="noopener noreferrer"&gt;The Statewave Guide Journey Index&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>architecture</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Statewave Guide — Day 2: Unknown is better than wrong</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Mon, 05 Oct 2026 19:24:05 +0000</pubDate>
      <link>https://dev.to/statewave/statewave-guide-day-2-unknown-is-better-than-wrong-3f0h</link>
      <guid>https://dev.to/statewave/statewave-guide-day-2-unknown-is-better-than-wrong-3f0h</guid>
      <description>&lt;p&gt;&lt;em&gt;Built 26 Aug 2026 · written up 5 Oct 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yesterday's question was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can we follow one real user action all the way from a button to the backend — without guessing?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Yes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But getting there was a useful reminder that even your benchmark can lie to you.&lt;/p&gt;

&lt;p&gt;The target looked simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New Client
    ↓
openCreateClient()
    ↓
NewClientDialog
    ↓
submitClient()
    ↓
clientService.create()
    ↓
POST /api/clients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frontend to backend.&lt;/p&gt;

&lt;p&gt;One connected application graph.&lt;/p&gt;

&lt;p&gt;No docs.&lt;/p&gt;

&lt;p&gt;No manually maintained knowledge base.&lt;/p&gt;

&lt;p&gt;Easy, right? 😅&lt;/p&gt;

&lt;p&gt;Our first quality run had other ideas.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;invokes&lt;/code&gt; precision: &lt;strong&gt;39%&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;API detection: &lt;strong&gt;19%&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;calls_api&lt;/code&gt;: &lt;strong&gt;0%&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Beautiful.&lt;/p&gt;

&lt;p&gt;At least the tests were honest. 😂&lt;/p&gt;

&lt;p&gt;Then something more interesting happened.&lt;/p&gt;

&lt;p&gt;We found what looked like an indexer bug.&lt;/p&gt;

&lt;p&gt;The benchmark expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /clients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The indexer produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/clients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first instinct was to normalize them.&lt;/p&gt;

&lt;p&gt;Turns out that would have been wrong.&lt;/p&gt;

&lt;p&gt;One adversarial fixture had another router with &lt;code&gt;/clients&lt;/code&gt; whose mount point could not be determined.&lt;/p&gt;

&lt;p&gt;Strip &lt;code&gt;/api&lt;/code&gt; from the known endpoint and suddenly two different routes become the same thing.&lt;/p&gt;

&lt;p&gt;Congratulations.&lt;/p&gt;

&lt;p&gt;We've manufactured product knowledge.&lt;/p&gt;

&lt;p&gt;So instead of fixing the indexer…&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;we fixed the benchmark.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That became another rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A benchmark isn't truth.&lt;br&gt;
It's an executable statement of what we currently believe truth is.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the evidence proves the benchmark wrong, the benchmark loses.&lt;/p&gt;

&lt;p&gt;We also introduced partial identities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api:GET:?/clients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meaning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I know this route exists. I do not know where it is mounted."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No guessing.&lt;/p&gt;

&lt;p&gt;No helpful little inference quietly connecting it to the wrong backend handler.&lt;/p&gt;

&lt;p&gt;After reconciliation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API precision:&lt;/strong&gt; 100%&lt;br&gt;
&lt;strong&gt;invokes:&lt;/strong&gt; 100% / 100%&lt;br&gt;
&lt;strong&gt;uses_hook:&lt;/strong&gt; 100% precision&lt;br&gt;
&lt;strong&gt;uses_service:&lt;/strong&gt; 100% precision&lt;br&gt;
&lt;strong&gt;submits_to:&lt;/strong&gt; 100%&lt;br&gt;
&lt;strong&gt;renders:&lt;/strong&gt; 100%&lt;br&gt;
&lt;strong&gt;validates_with:&lt;/strong&gt; 100%&lt;br&gt;
&lt;strong&gt;calls_api:&lt;/strong&gt; 83% precision / 100% recall&lt;/p&gt;

&lt;p&gt;Across the graph:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;0 relationships without evidence.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;0 invalid confidence values.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;0 unsupported inferences pretending to be facts.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;0 / 37 adversarial trap violations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;437 tests green.&lt;/p&gt;

&lt;p&gt;We also discovered our previous "deterministic output" wasn't actually deterministic.&lt;/p&gt;

&lt;p&gt;Same repository.&lt;/p&gt;

&lt;p&gt;Different working directory.&lt;/p&gt;

&lt;p&gt;Different graph.&lt;/p&gt;

&lt;p&gt;😬&lt;/p&gt;

&lt;p&gt;Eight working directories later:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;one hash.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fixed.&lt;/p&gt;

&lt;p&gt;One of the most important files we added might actually be &lt;a href="https://github.com/smaramwbc/statewave-guide/blob/main/docs/refusals.md" rel="noopener noreferrer"&gt;&lt;code&gt;refusals.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A catalogue of things the system intentionally refuses to infer.&lt;/p&gt;

&lt;p&gt;One real-world pattern would have generated dozens of fake API endpoints if we'd taken the obvious shortcut.&lt;/p&gt;

&lt;p&gt;So we didn't.&lt;/p&gt;

&lt;p&gt;That's increasingly becoming the philosophy behind Statewave Guide:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Missing knowledge is fixable.&lt;br&gt;
Wrong knowledge destroys trust.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So yes:&lt;/p&gt;

&lt;p&gt;We can follow a real user action all the way to the backend without guessing.&lt;/p&gt;

&lt;p&gt;Now comes the next problem.&lt;/p&gt;

&lt;p&gt;We know increasingly well &lt;strong&gt;what happened&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But not necessarily what those facts &lt;strong&gt;mean to the user&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statewave Guide — Day 2.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can a model add product meaning to this graph without being allowed to invent product truth?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This day in the repo: &lt;a href="https://github.com/smaramwbc/statewave-guide/pull/2" rel="noopener noreferrer"&gt;PR #2&lt;/a&gt; · Previous: &lt;a href="https://www.statewave.ai/blog/statewave-guide-day-1" rel="noopener noreferrer"&gt;Day 1 — The DOM is not the product&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Statewave Guide is built on &lt;strong&gt;Statewave&lt;/strong&gt; — an open-source, self-hosted memory runtime for AI agents. Governance and provenance, not just retrieval. Apache 2.0.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://statewave.ai" rel="noopener noreferrer"&gt;statewave.ai&lt;/a&gt; · &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;github.com/smaramwbc/statewave&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every post in this series: &lt;a href="https://www.statewave.ai/blog/statewave-guide" rel="noopener noreferrer"&gt;The Statewave Guide Journey Index&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>typescript</category>
      <category>ai</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Statewave Guide — Day 1: The DOM is not the product</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Wed, 30 Sep 2026 20:40:00 +0000</pubDate>
      <link>https://dev.to/statewave/statewave-guide-day-1-the-dom-is-not-the-product-1f4l</link>
      <guid>https://dev.to/statewave/statewave-guide-day-1-the-dom-is-not-the-product-1f4l</guid>
      <description>&lt;p&gt;&lt;em&gt;Built 26 Aug 2026 · written up 30 Sep 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yesterday I ended with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can source code become reliable product knowledge without developers maintaining another knowledge base beside it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer looks like &lt;strong&gt;yes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But definitely not by just reading the DOM.&lt;/p&gt;

&lt;p&gt;Day 0 ended with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;141 files&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;~17k lines added&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;257 tests passing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;and our first closed loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source code
    ↓
application graph
    ↓
running UI
    ↓
navigate
    ↓
highlight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So naturally I thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Nice. This is going surprisingly well."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That feeling lasted about five minutes. 😅&lt;/p&gt;

&lt;p&gt;Then we ran the implementation through adversarial review.&lt;/p&gt;

&lt;p&gt;Everything was green.&lt;/p&gt;

&lt;p&gt;And we still found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a project path containing &lt;code&gt;(&lt;/code&gt; silently producing an empty graph&lt;/li&gt;
&lt;li&gt;a React render loop happily heading past 300 renders&lt;/li&gt;
&lt;li&gt;a selector injection edge case that could escape toward something like &lt;code&gt;input[type="password"]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;React 19 ref-cleanup behavior we weren't handling correctly&lt;/li&gt;
&lt;li&gt;graph output changing depending on path capitalization&lt;/li&gt;
&lt;li&gt;and a beautiful JavaScript classic where &lt;code&gt;constructor&lt;/code&gt; inherited from &lt;code&gt;Object.prototype&lt;/code&gt; and quietly disappeared from our graph&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;257 tests.&lt;/p&gt;

&lt;p&gt;Still broken.&lt;/p&gt;

&lt;p&gt;I love software. 😂&lt;/p&gt;

&lt;p&gt;But the interesting part wasn't really the bugs.&lt;/p&gt;

&lt;p&gt;It was what they forced us to clarify architecturally.&lt;/p&gt;

&lt;p&gt;Eventually we want someone to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Where do I create a client?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;…and the application doesn't respond with six paragraphs of documentation.&lt;/p&gt;

&lt;p&gt;It takes you there.&lt;/p&gt;

&lt;p&gt;Highlights the control.&lt;/p&gt;

&lt;p&gt;Explains it.&lt;/p&gt;

&lt;p&gt;Eventually, it should even understand whether you've already done this twenty times and stop giving you the kindergarten tour.&lt;/p&gt;

&lt;p&gt;But first we need to answer a harder question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What does the application actually know about itself?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A browser DOM can tell us:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There's a button here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's nowhere near enough.&lt;/p&gt;

&lt;p&gt;We need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clients.create
      ↓
ClientsPage
      ↓
openCreateClient()
      ↓
NewClientDialog
      ↓
ClientForm
      ↓
submitClient()
      ↓
clientService.create()
      ↓
POST /api/clients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the difference between knowing the &lt;strong&gt;interface&lt;/strong&gt; and understanding the &lt;strong&gt;product&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So we made an important decision:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The DOM is runtime evidence.&lt;br&gt;
It is not the product model.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The product model starts with the source.&lt;/p&gt;

&lt;p&gt;Routes.&lt;br&gt;
Components.&lt;br&gt;
Functions.&lt;br&gt;
Forms.&lt;br&gt;
Services.&lt;br&gt;
APIs.&lt;br&gt;
Permissions.&lt;br&gt;
Schemas.&lt;br&gt;
Tests.&lt;/p&gt;

&lt;p&gt;Connected into an evidence-backed application graph.&lt;/p&gt;

&lt;p&gt;And the running UI connects back to that graph through stable semantic IDs like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;data-guide="clients.create"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No arbitrary CSS selectors.&lt;/p&gt;

&lt;p&gt;No hoping the third button inside the fourth &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; is still the third button after Friday's redesign.&lt;/p&gt;

&lt;p&gt;We also adopted a rule that I suspect is going to survive this whole project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Unknown is better than wrong.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If we can prove:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clients.create
    → invokes
    → openCreateClient
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we store it.&lt;/p&gt;

&lt;p&gt;With the file, symbol and line that prove it.&lt;/p&gt;

&lt;p&gt;If we &lt;em&gt;think&lt;/em&gt; something probably opens a dialog because the names look suspiciously similar?&lt;/p&gt;

&lt;p&gt;We store nothing.&lt;/p&gt;

&lt;p&gt;Missing knowledge can be improved later.&lt;/p&gt;

&lt;p&gt;Confidently wrong product knowledge is how you build an assistant that confidently takes users to the wrong screen.&lt;/p&gt;

&lt;p&gt;And once that happens twice, nobody trusts it again.&lt;/p&gt;

&lt;p&gt;That rule isn't new, by the way. It's borrowed from one layer down.&lt;/p&gt;

&lt;p&gt;In Statewave — the memory runtime underneath all of this — you don't write a memory directly. You record what happened, and memories are compiled from those events. Each one keeps the ids of the events it came from.&lt;/p&gt;

&lt;p&gt;We're holding the product's own source code to the same standard.&lt;/p&gt;

&lt;p&gt;So Day 1 isn't about chat.&lt;/p&gt;

&lt;p&gt;It's about reconstructing the real application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React UI
   ↓
handlers
   ↓
services
   ↓
HTTP
   ↓
Node API
   ↓
controllers
   ↓
business logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this works, developers may not need to maintain a separate product knowledge base at all.&lt;/p&gt;

&lt;p&gt;Because the product already contains one.&lt;/p&gt;

&lt;p&gt;We just normally call it &lt;strong&gt;source code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statewave Guide — Day 1.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can we follow one real user action all the way from a button to the backend — without guessing?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This day in the repo: &lt;a href="https://github.com/smaramwbc/statewave-guide/pull/1" rel="noopener noreferrer"&gt;PR #1&lt;/a&gt; · Previous: &lt;a href="https://www.statewave.ai/blog/statewave-guide-day-0" rel="noopener noreferrer"&gt;Day 0 — The Beginning&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Statewave Guide is built on &lt;strong&gt;Statewave&lt;/strong&gt; — an open-source, self-hosted memory runtime for AI agents. Governance and provenance, not just retrieval. Apache 2.0.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://statewave.ai" rel="noopener noreferrer"&gt;statewave.ai&lt;/a&gt; · &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;github.com/smaramwbc/statewave&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every post in this series: &lt;a href="https://www.statewave.ai/blog/statewave-guide" rel="noopener noreferrer"&gt;The Statewave Guide Journey Index&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>ai</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Statewave Guide — Day 0: The Beginning</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Mon, 28 Sep 2026 22:42:00 +0000</pubDate>
      <link>https://dev.to/statewave/statewave-guide-day-0-the-beginning-39h6</link>
      <guid>https://dev.to/statewave/statewave-guide-day-0-the-beginning-39h6</guid>
      <description>&lt;p&gt;&lt;em&gt;Built 25 Aug 2026 · written up 28 Sep 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The code is ahead of this story.&lt;/p&gt;

&lt;p&gt;Statewave Guide has been built in public for a few weeks now — one commit per build day, all of it on &lt;a href="https://github.com/smaramwbc/statewave-guide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What hasn't been public is the &lt;em&gt;why&lt;/em&gt;: nearly every day ended with something we got wrong, and a rule we took away from it.&lt;/p&gt;

&lt;p&gt;This series tells those days, in order, one or two a week. If you want the ending now, it's in the repo. The posts are about how we got there.&lt;/p&gt;

&lt;p&gt;Here's where it started.&lt;/p&gt;




&lt;p&gt;I wanted to add something fairly simple to an application:&lt;/p&gt;

&lt;p&gt;A help chat that actually knows how the product works.&lt;/p&gt;

&lt;p&gt;Not another little bubble in the bottom-right corner that politely links you to a 200-page documentation site nobody has opened since 2023.&lt;/p&gt;

&lt;p&gt;Something that can answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Where do I change this?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;…and instead of writing five paragraphs, it just takes you there.&lt;/p&gt;

&lt;p&gt;Highlights the right element.&lt;br&gt;
Explains what it does.&lt;br&gt;
Walks with you through the workflow.&lt;/p&gt;

&lt;p&gt;And eventually remembers that you already learned it last week, so it doesn't treat you like a new employee every Monday morning.&lt;/p&gt;

&lt;p&gt;Sounds simple.&lt;/p&gt;

&lt;p&gt;Obviously it isn't. 😅&lt;/p&gt;

&lt;p&gt;So I started looking around.&lt;/p&gt;

&lt;p&gt;There are great chat frameworks.&lt;br&gt;
Great product-tour libraries.&lt;br&gt;
Code documentation generators.&lt;br&gt;
RAG systems.&lt;br&gt;
Agent/UI protocols.&lt;/p&gt;

&lt;p&gt;Lots of very good pieces.&lt;/p&gt;

&lt;p&gt;But I couldn't find the thing I actually wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source code
    ↓
Product understanding
    ↓
Memory
    ↓
Interactive guidance
inside the real application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And one requirement became clear pretty quickly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I don't want developers maintaining a second version of their application in Confluence just so the assistant understands the first one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code should be the source of truth.&lt;/p&gt;

&lt;p&gt;Routes.&lt;br&gt;
Components.&lt;br&gt;
Forms.&lt;br&gt;
API endpoints.&lt;br&gt;
Schemas.&lt;br&gt;
Permissions.&lt;br&gt;
Tests.&lt;br&gt;
Translations.&lt;br&gt;
Git history.&lt;/p&gt;

&lt;p&gt;There is already a ridiculous amount of product knowledge sitting inside a modern codebase.&lt;/p&gt;

&lt;p&gt;We just don't normally call it documentation.&lt;/p&gt;

&lt;p&gt;So this is where the experiment starts.&lt;/p&gt;

&lt;p&gt;We're building our own guidance engine around Statewave.&lt;/p&gt;

&lt;p&gt;No dependency on an existing tour engine.&lt;/p&gt;

&lt;p&gt;We'll probably make some questionable architectural decisions.&lt;/p&gt;

&lt;p&gt;We'll definitely delete a few of them later.&lt;/p&gt;

&lt;p&gt;And I'll share the reasoning as we go — architecture, dead ends, trade-offs, weird bugs, and the occasional:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This was a much better idea yesterday."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The repository starts now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statewave Guide — Day 0.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can source code become reliable product knowledge without developers maintaining another knowledge base beside it?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This day in the repo: &lt;a href="https://github.com/smaramwbc/statewave-guide/commit/5cb3c8a" rel="noopener noreferrer"&gt;commit 5cb3c8a&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Statewave Guide is built on &lt;strong&gt;Statewave&lt;/strong&gt; — an open-source, self-hosted memory runtime for AI agents. Governance and provenance, not just retrieval. Apache 2.0.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://statewave.ai" rel="noopener noreferrer"&gt;statewave.ai&lt;/a&gt; · &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;github.com/smaramwbc/statewave&lt;/a&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Four of six open-source AI memory tools hold features back from the free build</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Mon, 28 Sep 2026 14:26:09 +0000</pubDate>
      <link>https://dev.to/statewave/four-of-six-open-source-ai-memory-tools-hold-features-back-from-the-free-build-388b</link>
      <guid>https://dev.to/statewave/four-of-six-open-source-ai-memory-tools-hold-features-back-from-the-free-build-388b</guid>
      <description>&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%2F583vb03qqu4ku46jzdh9.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%2F583vb03qqu4ku46jzdh9.png" alt="Four of six open-source AI memory tools hold features back from the free build" width="800" height="421"&gt;&lt;/a&gt;&lt;br&gt;
An Apache-2.0 badge does not tell you whether the feature you are switching for is in the free build. I counted 14 capabilities on &lt;a href="https://docs.mem0.ai/platform/platform-vs-oss" rel="noopener noreferrer"&gt;Mem0's own Platform vs Open Source page&lt;/a&gt; that self-hosted users do not get, then went through five alternatives that come up in the same conversation, one vendor page at a time. Four of the six hold something back.&lt;/p&gt;

&lt;p&gt;We build Statewave, one of the six, so weigh that entry accordingly. Every claim below links to the vendor's own page, our benchmark fork is public, and the section on our own tool ends with the things it does badly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mem0 is the default for good reasons
&lt;/h2&gt;

&lt;p&gt;Worth saying before the criticism, because most posts in this genre skip it. &lt;a href="https://github.com/mem0ai/mem0" rel="noopener noreferrer"&gt;Mem0&lt;/a&gt; takes three lines of code to add, ships Python and JavaScript SDKs, and offers a hosted API. It raised $24M in October 2025 and reported API calls going from 35 million in Q1 to 186 million in Q3 2025. CrewAI, Flowise and Langflow use it natively, AWS picked it as the exclusive memory provider for its Agent SDK, and the repo is past 65,000 stars. For personalizing a chat assistant, that is hard to beat.&lt;/p&gt;

&lt;p&gt;Trouble starts when you self-host, need an audit trail, or run several agents against one memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the self-hosted build stops
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The OSS build is a different product.&lt;/strong&gt; Mem0's own comparison page lists graph memory, memory decay, temporal reasoning, Dream background consolidation, webhooks, memory export, batch updates and deletes, feedback signals, summaries, custom categories, app-level scoping, orgs and projects, the project-wide event log and the web dashboard as Platform-only. Counted up, that is 14 capabilities, as of September 2026. Mem0's page also notes that the OSS SDK returns a not-supported error if you pass it the decay or temporal parameters.&lt;/p&gt;

&lt;p&gt;Graph memory is the clearest case. Moving the open-source SDK to the v3 pipeline deleted roughly 4,000 lines of external graph store drivers covering Neo4j, Memgraph, Kuzu, Apache AGE and Neptune. Its migration guide tells OSS users who need graph memory to move to the Platform. Mem0's README carries its own caveat: benchmark scores reflect the managed platform, "which includes proprietary optimizations not available in the open-source SDK."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graph memory and real retrieval volume start at $249 a month.&lt;/strong&gt; &lt;a href="https://mem0.ai/pricing" rel="noopener noreferrer"&gt;Mem0's plans&lt;/a&gt; as of September 2026: Hobby free at 1,000 retrievals a month, Starter $19 at 5,000, Pro $249 at 50,000. Graph memory and Dream consolidation begin at Pro. Retrieval limits bite well before storage limits do. A support agent handling 300 conversations a day with three lookups per conversation is around 27,000 retrievals a month, more than five times the Starter allowance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit logs, on-prem and SSO are Enterprise-only.&lt;/strong&gt; Self-hosting does not fill that gap. Its OSS edition keeps a history for each individual memory and no project-wide log of adds, searches and deletes. It scopes memory by &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;agent_id&lt;/code&gt; and &lt;code&gt;run_id&lt;/code&gt;, with no &lt;code&gt;app_id&lt;/code&gt; for separating tenants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You cannot reproduce what the agent saw.&lt;/strong&gt; Search returns whichever memories rank highest at the moment you query, and the OSS build does not store the exact set a given response was built from. When a customer asks why the agent told them their refund was approved, you are reading logs instead of looking up a record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edition line, across six tools
&lt;/h2&gt;

&lt;p&gt;Here is what each vendor's own documentation says, as of September 2026.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mem0&lt;/strong&gt;: 14 Platform-only capabilities by our count, graph memory removed from OSS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zep&lt;/strong&gt;: stopped maintaining Community Edition and moved its open-source work to &lt;a href="https://github.com/getzep/graphiti" rel="noopener noreferrer"&gt;Graphiti&lt;/a&gt;, which is a library rather than a memory service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cognee&lt;/strong&gt;: running the whole memory layer on Postgres ships as a demo feature. The production-ready version is a licensed product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supermemory&lt;/strong&gt;: the local build runs as one process on one machine with a single API key. Proprietary extraction models, multi-member orgs and connectors are Enterprise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Letta&lt;/strong&gt; and &lt;strong&gt;LangMem&lt;/strong&gt; ship their memory engines in full.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statewave&lt;/strong&gt; has no paid edition.
One question settles most of this: is the feature I am leaving Mem0 for included in the free build today, under the same license? Check the vendor's own edition or pricing page rather than a listicle. This one included.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Memory model&lt;/th&gt;
&lt;th&gt;You run&lt;/th&gt;
&lt;th&gt;Audit in the free build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mem0 OSS (Apache-2.0)&lt;/td&gt;
&lt;td&gt;Extracted facts + vector search&lt;/td&gt;
&lt;td&gt;Vector store, LLM, embedder&lt;/td&gt;
&lt;td&gt;Per-memory history only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statewave (Apache-2.0)&lt;/td&gt;
&lt;td&gt;Typed memories compiled from events&lt;/td&gt;
&lt;td&gt;Postgres + pgvector&lt;/td&gt;
&lt;td&gt;Source links, signed receipts, policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graphiti (Apache-2.0)&lt;/td&gt;
&lt;td&gt;Temporal knowledge graph&lt;/td&gt;
&lt;td&gt;Neo4j, FalkorDB or Neptune&lt;/td&gt;
&lt;td&gt;Historical graph edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Letta (Apache-2.0)&lt;/td&gt;
&lt;td&gt;Agent-edited memory blocks&lt;/td&gt;
&lt;td&gt;Letta App Server&lt;/td&gt;
&lt;td&gt;Git-tracked memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cognee (Apache-2.0)&lt;/td&gt;
&lt;td&gt;Graph + vector over docs and code&lt;/td&gt;
&lt;td&gt;Pluggable stores&lt;/td&gt;
&lt;td&gt;Inspectable evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangMem (MIT)&lt;/td&gt;
&lt;td&gt;Memory tools + background manager&lt;/td&gt;
&lt;td&gt;LangGraph store&lt;/td&gt;
&lt;td&gt;None built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supermemory local (MIT)&lt;/td&gt;
&lt;td&gt;Memory + RAG engine&lt;/td&gt;
&lt;td&gt;One local process&lt;/td&gt;
&lt;td&gt;Server logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The five alternatives, and the trade-off each one carries
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/getzep/graphiti" rel="noopener noreferrer"&gt;Graphiti&lt;/a&gt;&lt;/strong&gt; builds a knowledge graph that records when each relationship was true, so an agent can answer how a fact changed. In the Zep paper it beat MemGPT on DMR, 94.8% to 93.4%, and improved LongMemEval accuracy by up to 18.5% over baseline implementations. You pay for that in operations. Neo4j, FalkorDB or Neptune to run, Kuzu support deprecated, Zep's full memory service cloud-only now, and no receipts or access policies. Good fit for compliance and clinical agents, where "who owned this account before March?" is a real question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/letta-ai/letta" rel="noopener noreferrer"&gt;Letta&lt;/a&gt;&lt;/strong&gt; has the agent edit its own memory blocks, with context tracked in git and scheduled consolidation in the background. Its original repo is a landing page now, with active source in &lt;code&gt;letta-code&lt;/code&gt;. One honest data point they published themselves: Letta agents on gpt-4o-mini scored 74.0% on LoCoMo just by storing conversation history in files, which Letta read as a sign that current memory benchmarks may not mean much. It is a framework you adopt rather than a layer you drop into an existing agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;Cognee&lt;/a&gt;&lt;/strong&gt; turns text into entities and relationships and code into a graph of symbols, behind four operations: remember, recall, improve, forget. Plugins for Claude Code and Codex. Watch the edition line here, since running the whole memory layer on a single Postgres database is a demo feature in the open-source build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/langchain-ai/langmem" rel="noopener noreferrer"&gt;LangMem&lt;/a&gt;&lt;/strong&gt; is MIT and gives agents tools to save and search memory mid-conversation, plus a background manager that consolidates what they learn, natively against LangGraph's store. Two cautions: the default in-memory store loses everything on restart, so production needs a Postgres-backed store, and the &lt;a href="https://pypi.org/project/langmem/" rel="noopener noreferrer"&gt;last PyPI release&lt;/a&gt; was 0.0.30 in October 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/supermemoryai/supermemory" rel="noopener noreferrer"&gt;Supermemory&lt;/a&gt;&lt;/strong&gt; local puts memory and RAG behind the same API as the hosted platform, reports first place on LongMemEval and LoCoMo, and publishes MemoryBench so others can compare. Its local build is one process, one machine, one API key. Best for personal assistants and air-gapped experiments on a single box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our own entry, including where it loses
&lt;/h2&gt;

&lt;p&gt;Statewave is a memory runtime rather than a vector store. Raw agent events are saved as immutable episodes, a compiler turns them into typed memories carrying a confidence score, a validity window and the IDs of the episodes they came from, and at answer time it builds a ranked context bundle that fits a token budget you set. Given the same subject, task and budget over the same memory state, it returns the same bytes.&lt;/p&gt;

&lt;p&gt;We forked Mem0's own benchmark suite and changed only the memory backend, keeping Mem0's judge unchanged with gpt-4o as answerer and judge:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Statewave&lt;/th&gt;
&lt;th&gt;Mem0 cloud&lt;/th&gt;
&lt;th&gt;Mem0 OSS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LoCoMo (n=1,540)&lt;/td&gt;
&lt;td&gt;0.905&lt;/td&gt;
&lt;td&gt;0.899&lt;/td&gt;
&lt;td&gt;0.866&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LongMemEval (n=30)&lt;/td&gt;
&lt;td&gt;0.967&lt;/td&gt;
&lt;td&gt;0.933&lt;/td&gt;
&lt;td&gt;0.833&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read those carefully, because I do.&lt;/p&gt;

&lt;p&gt;Each is a single run rather than an average. Our LoCoMo margin over Mem0 cloud is 0.006, and a rerun can land either side of it, so nobody should treat it as a win. LongMemEval ran 30 questions, which makes it directional at best.&lt;/p&gt;

&lt;p&gt;Two things cut against us specifically. We applied three fixes to Mem0's client code and every one of them raised Mem0's scores. And Mem0 OSS retrieves at most 20 memories per query by default, against roughly 200 for Statewave and Mem0 cloud, so part of that gap is retrieval budget rather than ranking quality. Which makes the cloud column the fairer comparison of the two.&lt;/p&gt;

&lt;p&gt;Mem0 also publishes higher figures for its newer algorithm, 92.5 on LoCoMo and 94.4 on LongMemEval, from a different model stack than our fork runs. &lt;a href="https://www.statewave.ai/benchmarks" rel="noopener noreferrer"&gt;The full methodology is public&lt;/a&gt; and the fork diffs against upstream.&lt;/p&gt;

&lt;p&gt;Where it loses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Self-hosted only, with no managed cloud.&lt;/li&gt;
&lt;li&gt;No first-class entity graph. If your questions are graph questions, Graphiti is the better tool.&lt;/li&gt;
&lt;li&gt;One Postgres database. Multiple API replicas are verified, cross-region clustering is not available.&lt;/li&gt;
&lt;li&gt;Tenant isolation is enforced in the application, without Postgres row-level security.&lt;/li&gt;
&lt;li&gt;Bundles are denser than a plain fact store, so each answer costs more tokens.
Best for teams whose memory has to stay in their own infrastructure and be explainable to an auditor or a customer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Four questions that settle most of these evaluations
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is the feature you are switching for in the free build today, under the same license?&lt;/strong&gt; Edition pages get revised, and features move across the line in both directions. Check the date on whatever you are reading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can you prove what the agent saw?&lt;/strong&gt; If an auditor or a customer might ask, you need links back to source events and a per-call record, and you need them on the retrieval path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many systems do you want to patch?&lt;/strong&gt; A graph database, a vector database and an LLM is three.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can you clone and rerun the benchmark?&lt;/strong&gt; One you can rerun beats a chart you cannot.
Pick by requirement rather than star count. Fastest real test is to replay a week of your own conversations through two backends and diff the context bundles side by side. Whatever the leaderboards say, that diff is the thing you will actually be shipping.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;Statewave's core runtime is on GitHub&lt;/a&gt; under Apache-2.0 if you want to read the compiler and the ranking code rather than take the table's word for it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>Why our support health score drops 25 points on a quiet week</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Mon, 28 Sep 2026 14:18:53 +0000</pubDate>
      <link>https://dev.to/statewave/why-our-support-health-score-drops-25-points-on-a-quiet-week-12j0</link>
      <guid>https://dev.to/statewave/why-our-support-health-score-drops-25-points-on-a-quiet-week-12j0</guid>
      <description>&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%2Ftqwz1xho1jy08jxlb5o7.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%2Ftqwz1xho1jy08jxlb5o7.png" alt="Why our support health score drops 25 points on a quiet week" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
I scored one support account once a day for eleven days and added nothing to it. No new tickets, no messages, no resolutions. It went from 80 to 55 and crossed a band boundary on the way down. Two of the eight signals in the score read the clock, and that is the whole explanation.&lt;/p&gt;

&lt;p&gt;We build Statewave, an open-source memory runtime for AI support agents, so treat the framing as ours and the numbers as checkable. The scoring rules are plain arithmetic in the Apache-2.0 core, which is what makes this post possible: every number below can be re-derived from the published constants.&lt;/p&gt;
&lt;h2&gt;
  
  
  What this score measures, and what it leaves to your CSM tool
&lt;/h2&gt;

&lt;p&gt;Worth separating before anything else, because the two answer different questions.&lt;/p&gt;

&lt;p&gt;Customer success platforms score the whole relationship. &lt;a href="https://support.gainsight.com/Staircase_AI/Configurations/Staircase_AI_Health_Score" rel="noopener noreferrer"&gt;Gainsight's Staircase AI health score&lt;/a&gt; combines sentiment, engagement, open items and response time, with weights set by a statistical model and lifecycle modifiers layered on top.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://knowledge.hubspot.com/help-desk/customize-a-health-score-in-the-customer-success-workspace" rel="noopener noreferrer"&gt;HubSpot's customer success workspace&lt;/a&gt; takes a different route: an admin builds a points-based score from event and property groups, with a point cap per group and optional decay.&lt;/p&gt;

&lt;p&gt;Ours reads only the support record: open and resolved sessions, urgency in the conversation, idle tickets, response and resolution times. Narrower on purpose, because it matches the question the next agent has at escalation: how are this customer's tickets going right now? Predicting renewal is a different job and this number will not do it.&lt;/p&gt;
&lt;h2&gt;
  
  
  How the eight rules compute the number
&lt;/h2&gt;

&lt;p&gt;Every customer starts at 100. Eight rules move the number, the result is clamped between 0 and 100, and a band is assigned: 70 and above is &lt;code&gt;healthy&lt;/code&gt;, 40 to 69 is &lt;code&gt;watch&lt;/code&gt;, under 40 is &lt;code&gt;at_risk&lt;/code&gt;.&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;Points&lt;/th&gt;
&lt;th&gt;Cap&lt;/th&gt;
&lt;th&gt;Fires when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unresolved issues&lt;/td&gt;
&lt;td&gt;-15 per open session&lt;/td&gt;
&lt;td&gt;-45&lt;/td&gt;
&lt;td&gt;A session's resolution status is open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeated issue&lt;/td&gt;
&lt;td&gt;-20&lt;/td&gt;
&lt;td&gt;Once&lt;/td&gt;
&lt;td&gt;An open session shares at least 30% of its keywords with a resolved session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalations&lt;/td&gt;
&lt;td&gt;-10 per episode&lt;/td&gt;
&lt;td&gt;-20&lt;/td&gt;
&lt;td&gt;A message contains an urgency marker such as outage, p0, sev1, or escalat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle open issue&lt;/td&gt;
&lt;td&gt;-15&lt;/td&gt;
&lt;td&gt;Once&lt;/td&gt;
&lt;td&gt;An open session has had no activity for 7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolution breach&lt;/td&gt;
&lt;td&gt;-10 per session&lt;/td&gt;
&lt;td&gt;-20&lt;/td&gt;
&lt;td&gt;A resolved session took more than 24 hours from the first customer message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow first response&lt;/td&gt;
&lt;td&gt;-5&lt;/td&gt;
&lt;td&gt;Once&lt;/td&gt;
&lt;td&gt;Average first response across sessions is above 10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recent resolution&lt;/td&gt;
&lt;td&gt;+10&lt;/td&gt;
&lt;td&gt;Once&lt;/td&gt;
&lt;td&gt;A session was resolved in the last 7 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High resolution rate&lt;/td&gt;
&lt;td&gt;+10&lt;/td&gt;
&lt;td&gt;Once&lt;/td&gt;
&lt;td&gt;At least 80% of two or more sessions are resolved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No model call, no stored score, nothing cached in the scoring path. The same history at the same moment always returns the same number and the same factor list.&lt;/p&gt;
&lt;h2&gt;
  
  
  The factor list matters more than the number
&lt;/h2&gt;

&lt;p&gt;Take a constructed mid-market account with three tickets. An SSO login loop fixed in two hours, three weeks ago. A duplicate-invoice export problem that took 36 hours to close last week. A warehouse sync to Snowflake failing since a schema change, untouched for seven days.&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;"subject_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;"acct_ridgeline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"watch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"factors"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"signal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unresolved_issues"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1 open session(s)"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"signal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"idle_open_issue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Session t-102 idle for 7 days"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"signal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recent_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;"impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1 session(s) resolved in last 7 days"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"signal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sla_resolution_breaches"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1 session(s) exceeded 24h resolution SLA"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"signal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slow_first_response"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Avg first response 10.7 min (threshold: 10 min)"&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;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;A receiving agent does not have to trust 65. They can see that 30 of the 45 penalty points come from one stale ticket, and go straight to &lt;code&gt;t-102&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fixed rules beat learned weights for a number that lands in an escalation brief, for one reason. Nobody can argue with a learned weight at 2am. A -15 labeled &lt;code&gt;Session t-102 idle for 7 days&lt;/code&gt; gets checked in ten seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it drifts when nothing happens
&lt;/h2&gt;

&lt;p&gt;Two signals read elapsed time. An idle penalty fires once an open ticket passes seven days without activity, and the recent-resolution bonus expires seven days after a ticket closes. Neither needs a new event to change the score.&lt;/p&gt;

&lt;p&gt;Scored daily across eleven days with no new episodes and no new resolutions, that same account walks down on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Day 0: &lt;strong&gt;80&lt;/strong&gt;, healthy&lt;/li&gt;
&lt;li&gt;Day 3: the Snowflake ticket crosses seven idle days, &lt;strong&gt;65&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Day 7: last week's invoice fix ages out of the bonus window, &lt;strong&gt;55&lt;/strong&gt;
Urgency markers behave the opposite way. Nothing ages them out, because the escalation check counts matching episodes regardless of when they were written.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So an "outage" message from a ticket that closed months ago keeps costing points forever. Only the 20-point cap bounds how much.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alerts fire only when something asks for the score
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;subject.health_degraded&lt;/code&gt; and &lt;code&gt;subject.health_improved&lt;/code&gt; webhooks compare the current band against the last band cached. That comparison runs only when the score is computed, which happens on &lt;code&gt;GET /v1/subjects/{subject_id}/health&lt;/code&gt; and on &lt;code&gt;POST /v1/handoff&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the walkthrough above, the drop to &lt;code&gt;watch&lt;/code&gt; on day 3 sends nothing at all until one of those calls runs. If you want band-change alerts on the day they happen, run a scheduled job that requests health for every subject with an open session.&lt;/p&gt;

&lt;p&gt;One more detail worth knowing: a subject scored for the first time is compared against a default of &lt;code&gt;healthy&lt;/code&gt;. A new subject whose first score lands in &lt;code&gt;watch&lt;/code&gt; therefore fires &lt;code&gt;health_degraded&lt;/code&gt; immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can actually reach at_risk
&lt;/h2&gt;

&lt;p&gt;I enumerated every combination of signal values the rules allow, respecting dependencies such as a repeated issue requiring at least one open session, and scored each one against the published constants. Doing so produced 936 feasible combinations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No single signal can do it.&lt;/strong&gt; Largest penalty any one signal applies is 45, from three or more open sessions, which leaves a score of 55. Still &lt;code&gt;watch&lt;/code&gt;. Every &lt;code&gt;at_risk&lt;/code&gt; customer has at least two separate things going wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero open tickets means a floor of 55.&lt;/strong&gt; With no open sessions the unresolved, repeated-issue and idle signals cannot fire at all. Everything left adds up to 45 at most, and 66 of the 72 no-open-ticket combinations come out healthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fastest route is three open tickets plus one full-strength signal.&lt;/strong&gt; Three open sessions (-45) plus a repeated issue (-20), two urgency episodes (-20), or two resolution breaches (-20) lands on exactly 35. With only one open ticket, reaching &lt;code&gt;at_risk&lt;/code&gt; takes at least three other signals firing together.&lt;/p&gt;

&lt;p&gt;Read an &lt;code&gt;at_risk&lt;/code&gt; alert as "this customer's queue needs a person today." Risks that live outside the queue, like a champion leaving or a renewal slipping, have to reach your team some other way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ingest mistakes that move the score with no error
&lt;/h2&gt;

&lt;p&gt;Each of these is a mismatch between how your ingest path records support data and the fields the rules actually read. None of them raise an error. I reproduced all four against the scoring rules before writing them down.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;Why it happens&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;An outage ticket scores like a routine one&lt;/td&gt;
&lt;td&gt;The escalation and repeated-issue checks read &lt;code&gt;payload.messages[].content&lt;/code&gt;. Text sent as &lt;code&gt;payload.text&lt;/code&gt; is skipped. An open ticket reading "Full outage, P0" scored 85 as text and 75 as messages.&lt;/td&gt;
&lt;td&gt;Record support turns in the messages shape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A 48-hour resolution costs no points&lt;/td&gt;
&lt;td&gt;Timing checks recognize &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;chat&lt;/code&gt; and &lt;code&gt;support-chat&lt;/code&gt; as customer sources, and &lt;code&gt;assistant&lt;/code&gt;, &lt;code&gt;agent&lt;/code&gt;, &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;tool&lt;/code&gt; as responders. The same history scored 100 with &lt;code&gt;customer&lt;/code&gt;/&lt;code&gt;staff&lt;/code&gt; and 95 with &lt;code&gt;user&lt;/code&gt;/&lt;code&gt;agent&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Normalize source names at ingest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing questions count as escalations&lt;/td&gt;
&lt;td&gt;Urgency markers are substring matches, so "download" contains "down"&lt;/td&gt;
&lt;td&gt;Never alert on the escalations factor alone. The 20-point cap limits the damage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scores run high right after a bulk import&lt;/td&gt;
&lt;td&gt;Idle and timing checks read &lt;code&gt;created_at&lt;/code&gt;, which is ingest time. Backfilled tickets look answered in seconds and resolved today&lt;/td&gt;
&lt;td&gt;Backfill before go-live, or hold health alerts for seven days after an import&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One failure we see more often than any of those is not in the table at all: tickets that never get closed. Open-session penalties stack toward 45 and the resolution bonuses never fire, so long-standing customers drift into &lt;code&gt;watch&lt;/code&gt; for no reason other than bookkeeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the handoff pack drops first
&lt;/h2&gt;

&lt;p&gt;A score is one line in a larger brief. &lt;code&gt;POST /v1/handoff&lt;/code&gt; assembles nine sections from stored memory, with no model call, then trims to a token budget: 4,000 by default, up to 16,000.&lt;/p&gt;

&lt;p&gt;Trimming cuts from the end. Customer and health come first so they survive, and recent context from the customer's other sessions is the first thing lost. If your briefs are getting cut, raise &lt;code&gt;max_tokens&lt;/code&gt; before you change anything else.&lt;/p&gt;

&lt;p&gt;Three quirks worth knowing before you wire this into an escalation path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The health line keeps the first three factors rather than the biggest three.&lt;/strong&gt; Factors are added in the fixed order the rules run, so for the account above that means the open session, the idle ticket and the recent resolution. Its resolution breach and slow first response reach the next agent through the SLA section instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active Issue is the first message in the session, cut to 200 characters.&lt;/strong&gt; If the session opens with "hi", so does the brief. Record the problem statement, or a ticket-created event carrying the subject line, as the first episode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What Has Been Tried keeps five entries, oldest first.&lt;/strong&gt; On a long-running ticket the most recent attempts are the ones left out, so a specialist picking one up should pull the full session timeline for anything past step five.
Because nothing is generated, the brief cannot invent a detail that was never recorded. Which cuts both ways. A model-written summary reads more smoothly, and it can also tell you something that never happened.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you are building your own
&lt;/h2&gt;

&lt;p&gt;Five things that transfer, whatever you build this on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make every point traceable to a reason string.&lt;/strong&gt; A score that goes into an escalation brief has to survive a skeptical human during a live incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide deliberately which signals age.&lt;/strong&gt; Two of ours do, and both surprised us once we watched them over a week: bonuses that expire, and urgency markers that never expire at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute on demand and schedule the computation.&lt;/strong&gt; If your alerts compare against a cached band, they fire when something asks, not when the state changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your ingest shape is what the score actually reads.&lt;/strong&gt; Field names and payload shapes change the number silently, whatever you meant them to record. Normalize at ingest, and write a resolution when a ticket closes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap anything matched by substring.&lt;/strong&gt; "download" contains "down", and a cap is what stops that from becoming an alert.
Start with one account that has an open ticket. Call the health endpoint and check each factor against the ticket history. It takes about ten minutes and it tells you whether your ingest path records what the rules actually read.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Scoring rules, handoff assembly and the eval suite all live in &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;the Apache-2.0 core runtime&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Read the constants there rather than taking our word for any of this.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>python</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Your dedup marker expires. The fact it created does not.</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Mon, 14 Sep 2026 12:04:13 +0000</pubDate>
      <link>https://dev.to/statewave/your-dedup-marker-expires-the-fact-it-created-does-not-3lia</link>
      <guid>https://dev.to/statewave/your-dedup-marker-expires-the-fact-it-created-does-not-3lia</guid>
      <description>&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%2Fue7ybjejl9g2b583e4v3.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%2Fue7ybjejl9g2b583e4v3.png" alt="Your dedup marker expires. The fact it created does not." width="800" height="420"&gt;&lt;/a&gt;Here is a bug that raises no error anywhere and shows up as a memory store slowly filling with the same fact twice.&lt;/p&gt;

&lt;p&gt;You process an event, write a dedup marker, and set a 24-hour TTL on it so the marker table does not grow forever. Every guide tells you to do this, and for delivery-level idempotency it is correct advice. Then a backfill re-reads a ticket from last year. The marker expired months ago, so the ingest layer has no memory of it. But the fact derived from that event is still in the store, because derived facts do not expire on a schedule.&lt;/p&gt;

&lt;p&gt;Now you have it twice. Nothing failed.&lt;/p&gt;

&lt;p&gt;I went through the papers, talks and practitioner write-ups we had collected on this, and the word "idempotent" carried four incompatible meanings across them. Every source recommending a dedup marker also recommended a TTL on it. None discussed the interaction between an expiring marker and a non-expiring derived fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four definitions sharing one word
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Request-level.&lt;/strong&gt; Repeating the same request leaves state unchanged after the first. The HTTP version is familiar, and &lt;a href="https://www.rfc-editor.org/info/rfc9110/" rel="noopener noreferrer"&gt;RFC 9110&lt;/a&gt; defines it in section 9.2.2: GET and DELETE are naturally idempotent, POST is not, and PATCH depends entirely on what it does. A PATCH setting a name to "DJ" is idempotent. A PATCH incrementing a counter is not. Same verb, opposite guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delivery-level.&lt;/strong&gt; The same event arriving twice gets acted on once. Most event-driven systems mean this, and enforce it with a marker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merge-level.&lt;/strong&gt; The merge function is itself idempotent, plus associative and commutative, so replicas converge regardless of arrival order or repetition. This is the &lt;a href="https://crdt.tech/" rel="noopener noreferrer"&gt;CRDT&lt;/a&gt; property.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Derivation-level.&lt;/strong&gt; Re-running a derivation over the same inputs produces no new derived records. This is the one that matters for agent memory, and the one almost nobody names.&lt;/p&gt;

&lt;p&gt;Satisfying any of those tells you nothing about the other three. A system can be fully delivery-idempotent and still accumulate duplicate facts, by exactly the mechanism above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why memory pipelines re-read on purpose
&lt;/h2&gt;

&lt;p&gt;Retries are the obvious source. Even a broker promising &lt;a href="https://kafka.apache.org/documentation/#semantics" rel="noopener noreferrer"&gt;exactly-once delivery&lt;/a&gt; cannot stop an upstream service publishing the same logical event twice under two different event IDs, which is why ID checks usually get paired with a business-key check on something like an order number.&lt;/p&gt;

&lt;p&gt;What is specific to memory systems is that re-syncs are not failures. A connector pulling GitHub issues, Slack threads or support tickets runs on a schedule and re-reads overlapping windows every time, by design. A backfill re-reads everything. Neither is an error condition, so treating duplicate suppression as error handling puts the check in the wrong place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Derive the key, never expire it
&lt;/h2&gt;

&lt;p&gt;Stop generating keys and start deriving them from the event's logical identity. A derived key costs nothing to keep forever, because it is a property of the record rather than a row in a growing side table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github:acme:api-server:issue:4471:opened
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source, org, repo, record type, issue number, event kind. Re-sync that issue a thousand times and you get one episode. No UUID, no timestamp, no TTL.&lt;/p&gt;

&lt;p&gt;One file over in the same connector suite, the IDE companion uses the opposite rule: idempotency is content-addressable, so re-running an unchanged workspace scan maps to the same key and a changed workspace yields a new memory.&lt;/p&gt;

&lt;p&gt;Two opposite policies in one codebase, and both are right. A GitHub issue has a stable identity independent of its text, so keying on identity is correct and an edited title should not create a second episode. A workspace scan has no identity apart from its contents, so keying on a content hash is correct and a changed workspace should produce a new one.&lt;/p&gt;

&lt;p&gt;Your key encodes a definition of "the same event," and that definition is a per-source decision. Wrong in one direction floods the store. Wrong in the other silently drops real updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compilation has to set, not append
&lt;/h2&gt;

&lt;p&gt;Ingest dedup only covers ingest. The compile step needs its own guarantee, and it comes from one design choice: a compile pass that derives current state from the full episode log behaves like a set. A pass that appends whatever it found this run behaves like an increment, and increments are never idempotent.&lt;/p&gt;

&lt;p&gt;That single property is what makes a compile endpoint safe to call from a retry loop, a cron job and a webhook handler at the same time. It is also what lets you batch compilation off the request path instead of running it inline after every turn.&lt;/p&gt;

&lt;p&gt;Test for it in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_recompile_is_a_noop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/memories/compile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;subject_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memories_created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/memories/compile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;subject_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memories_created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That whole test is the second number. If it is not zero, you have an append where you need a derivation, and no amount of ingest deduplication will save you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplicates and conflicts are different problems
&lt;/h2&gt;

&lt;p&gt;A duplicate is two records saying the same thing, and the fix is to keep one. A conflict is two records saying different things, both legitimately written, and there is no version of "keep one" that is obviously right.&lt;/p&gt;

&lt;p&gt;Idempotency does nothing for the second case. Martin Kleppmann's &lt;a href="https://speakerdeck.com/ept/conflict-resolution-for-eventual-consistency" rel="noopener noreferrer"&gt;taxonomy of conflict handling&lt;/a&gt; is still the cleanest: let a human resolve it, pick a winner automatically, or merge automatically. On the middle option he is precise about the cost, noting that some systems choose one version as the winner and throw away the others.&lt;/p&gt;

&lt;p&gt;Throwing away the loser is the default in most agent memory stacks, and it is where the damage lands. You do not just lose the old value. You lose the evidence that a disagreement existed at all.&lt;/p&gt;

&lt;p&gt;Recency-wins is near-universal as the automatic rule, and it is fine for state that genuinely changes, like which database a team runs. It is wrong for a stable attribute that a bad extraction re-asserts incorrectly. And it is wrong for facts that only look contradictory: "I am in Berlin this week" does not contradict "this user lives in Lisbon," and a system that supersedes the second with the first has made the memory worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supersede, because the loser is evidence
&lt;/h2&gt;

&lt;p&gt;Mark the older fact superseded, filter it out at read time, keep it in the record with provenance links to both sides. Three states cover it: active, superseded, tombstoned.&lt;/p&gt;

&lt;p&gt;A worked example I can point at: three agents read three source documents concurrently and write to one shared subject. One commits Stripe's pre-reversal processing rate, another commits the corrected rate. Where the two memories carry a registered single-valued claim key, the compiler compares claims directly. Otherwise it falls back to Jaccard word overlap between the compiled memories, and at 0.6 or above it marks the older one superseded and records the decision with links to both source episodes.&lt;/p&gt;

&lt;p&gt;Downstream, the synthesis agent's bundle contains only the winner. The audit trail contains both, plus the similarity score that triggered the call, 0.72 in the shipped run.&lt;/p&gt;

&lt;p&gt;Treat 0.6 as a dial rather than a constant. Set it lower and unrelated facts collide, producing false-positive supersession that deletes true information from the read path. Set it higher and genuine contradictions both survive into the bundle, so you pay tokens to hand the model two incompatible answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this still fails
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Word overlap is a fallback, not the mechanism.&lt;/strong&gt; Jaccard catches "Stripe charges 3.5% + 35c" against "Stripe charges 2.9% + 30c." It will occasionally fire on unrelated facts sharing common words, and it misses contradictions expressed in entirely different vocabulary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent is not deterministic.&lt;/strong&gt; Run an LLM compiler and the extraction step can produce different facts from the same episodes on different runs. Idempotent compilation means a second pass over already-processed episodes adds nothing. It does not mean the first pass is reproducible. A regex-based heuristic compiler is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some conflicts should not be resolved.&lt;/strong&gt; Two high-confidence claims that contradict each other may mean the boundary conditions differ. If being wrong is expensive in your domain, flag them and abstain instead of auto-resolving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;None of this fixes bad extraction.&lt;/strong&gt; Deduplicating, superseding and auditing low-quality facts gives you a clean, well-audited store of low-quality facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ten-minute check
&lt;/h2&gt;

&lt;p&gt;Run your compile step twice in a row and look at the second number. If it created anything, that is the bug to fix before any of the rest.&lt;/p&gt;

&lt;p&gt;If you would rather not build the key derivation, compile markers and supersession logic, the runtime behind the examples here is &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;Apache-2.0 on GitHub&lt;/a&gt; and runs on your own Postgres. The &lt;a href="https://www.statewave.ai/blog/idempotent-compilation-and-conflict-resolution" rel="noopener noreferrer"&gt;full version of this post&lt;/a&gt; covers the per-kind conflict policy table and the replay boundary I skipped.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>postgres</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Quadrupling my agent's context budget bought 3x the facts, not 4x</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Mon, 14 Sep 2026 11:57:56 +0000</pubDate>
      <link>https://dev.to/statewave/quadrupling-my-agents-context-budget-bought-3x-the-facts-not-4x-28e8</link>
      <guid>https://dev.to/statewave/quadrupling-my-agents-context-budget-bought-3x-the-facts-not-4x-28e8</guid>
      <description>&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%2Fsda7pokkvxl9g03ll39q.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%2Fsda7pokkvxl9g03ll39q.png" alt="Quadrupling my agent's context budget bought 3x the facts, not 4x" width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
I ran one subject through a context assembler at three different token budgets and wrote down what came back. 200 tokens returned 2 memories. 500 returned 4. 800 returned 6.&lt;/p&gt;

&lt;p&gt;That is a 4x spend for 3x the facts. It is the most useful number I have for sizing an agent's memory budget, and I almost never see it published.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Budget requested&lt;/th&gt;
&lt;th&gt;Tokens returned&lt;/th&gt;
&lt;th&gt;Utilization&lt;/th&gt;
&lt;th&gt;Memories returned&lt;/th&gt;
&lt;th&gt;Marginal cost per new memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;178&lt;/td&gt;
&lt;td&gt;89.0%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;89 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;88.6%&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;133 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;761&lt;/td&gt;
&lt;td&gt;95.1%&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;159 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Method, so you can repeat it: these are the published outputs of the &lt;code&gt;budget&lt;/code&gt; command in the &lt;code&gt;statewave-personal-assistant&lt;/code&gt; reference app, which calls the context API three times at three budgets against the same subject. I read them on 3 August 2026 and derived the marginal column myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheap facts get spent first
&lt;/h2&gt;

&lt;p&gt;Marginal cost per fact rose 79% across that range, from 89 tokens to 159.&lt;/p&gt;

&lt;p&gt;The assembler is not distributing effort evenly. It sorts by score and fills from the top, so the first two items are the densest, highest-priority things it holds. By the fifth and sixth it has worked down into longer, lower-scoring material.&lt;/p&gt;

&lt;p&gt;So if you are choosing between 400 and 800 tokens, you are not choosing between four and eight facts. You are deciding whether to pay roughly double for two more items that already scored worse than everything in the bundle.&lt;/p&gt;

&lt;p&gt;Utilization is the other column worth reading. It landed at 89.0%, 88.6% and 95.1%, and it never crossed 100%, because items are admitted whole. A memory that would overshoot the ceiling by one token gets dropped rather than truncated mid-sentence. Headroom across those runs was 11.0%, 11.4% and 4.9%. Plan for 5 to 11% of slack instead of assuming you get the number you asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stop rule is three lines, and that is the point
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token_count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A greedy knapsack fill. &lt;a href="https://arxiv.org/abs/2508.04903" rel="noopener noreferrer"&gt;RCR-Router&lt;/a&gt;, a multi-agent routing paper, publishes essentially this as its Algorithm 1: sort by importance, accumulate, break on overflow. Nothing in it is stochastic, which is the property everything below depends on.&lt;/p&gt;

&lt;p&gt;Compare it against the two patterns it replaces. Full history replay re-sends everything each turn, so cost grows with session lifetime and a throwaway comment carries the same weight as a production incident. Top-K vector retrieval returns the K nearest chunks, but K is a count and not a token budget, so your actual prompt size drifts with chunk length.&lt;/p&gt;

&lt;p&gt;And no, a bigger window does not make this go away. Liu et al. tested position directly in &lt;a href="https://aclanthology.org/2024.tacl-1.9/" rel="noopener noreferrer"&gt;Lost in the Middle&lt;/a&gt; and found accuracy follows a U-shaped curve against where the relevant passage sits: best at the start and end, worst in the middle, and that held even for models built for long context. Padding a 128K window with 100K tokens of history hands the model a large middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoring is where your opinions live
&lt;/h2&gt;

&lt;p&gt;The fill is trivial. Ranking is where the design decisions sit. Four signals carry most of the ordering in the implementation those numbers came from:&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;Range&lt;/th&gt;
&lt;th&gt;What it encodes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kind priority&lt;/td&gt;
&lt;td&gt;3 to 10&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;profile_fact&lt;/code&gt; 10, &lt;code&gt;procedure&lt;/code&gt; 8, &lt;code&gt;episode_summary&lt;/code&gt; 5, &lt;code&gt;raw_episode&lt;/code&gt; 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recency&lt;/td&gt;
&lt;td&gt;0 to 5&lt;/td&gt;
&lt;td&gt;Linear, the newest memory takes the maximum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task relevance&lt;/td&gt;
&lt;td&gt;0 to 8&lt;/td&gt;
&lt;td&gt;Word overlap contributes up to 5, cosine similarity up to 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporal validity&lt;/td&gt;
&lt;td&gt;-4 to +3&lt;/td&gt;
&lt;td&gt;Currently valid adds 3, expired subtracts 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that as a set of opinions rather than a spec. A stable profile fact outranks a raw conversation turn by 7 points on kind alone, before any other signal applies, which is why identity survives budget pressure and chatter does not. An expired fact carries a 7-point swing against it. Cosine similarity is in there, but as one input among four rather than the whole ranking.&lt;/p&gt;

&lt;p&gt;What makes small budgets viable at all is compilation: a pass over raw episodes producing typed facts with confidence scores and provenance back to the source events. Two hundred conversation turns become one &lt;code&gt;profile_fact&lt;/code&gt;. Rank raw turns instead and you are sorting a bad unit, low density and high token count with no confidence signal attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Determinism matters more than the compression
&lt;/h2&gt;

&lt;p&gt;A 73% token reduction is the headline here, from roughly 2,800 tokens of raw history down to 761. I care more that it is the same 761 tokens every time.&lt;/p&gt;

&lt;p&gt;Researchers at Penn State measured the alternative. Their work on parallel context compaction reports that as context grows, both the volume a model produces and the information it retains vary run to run. Prompt instructions asking for a specific summary length get largely ignored.&lt;/p&gt;

&lt;p&gt;Which means: if your compaction step is an LLM call, your retrieval layer is non-deterministic, and every eval you run afterwards measures two things at once.&lt;/p&gt;

&lt;p&gt;Deterministic assembly means same subject, same task string, same budget, same point in time, same bytes. When an answer changes, you know the change came from the model or the prompt, because the context did not move.&lt;/p&gt;

&lt;p&gt;There is a real cost attached. Compile-then-use bundles are denser per answer than a plain fact-store lookup. If your queries are mostly single-hop and cost-sensitive, a lighter store may be the better call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six steps for setting your own number
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Instrument before you tune.&lt;/strong&gt; Log the returned token estimate and memory count on every context call. You want the utilization ratio. Running consistently below 85% means the budget is not your constraint and raising it will change nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start at 800.&lt;/strong&gt; Aim for 5 to 8 compiled facts, which for typical fact and procedure sizes lands between 600 and 1,200 tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the fixed costs before deciding it is too small.&lt;/strong&gt; System prompt and tool schemas run 2,000 to 4,000 tokens on their own and get billed every call. Memory is usually the smallest line item in a well-built agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the marginal curve, not the total.&lt;/strong&gt; Run your own version of the three-budget test above and find where the next 200 tokens stop changing answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set per-role budgets for multi-agent runs.&lt;/strong&gt; A base budget plus a role offset. A planner needs structured plans; an executor needs less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep compaction off the hot path.&lt;/strong&gt; Compile on a schedule, every N episodes or nightly, using an async mode that hands you a job ID to poll.
Raise the budget only when a named eval fails at the current one.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Two ways this still goes wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bad extraction upstream.&lt;/strong&gt; Ranking garbage precisely and fitting it exactly into 800 tokens gives you 800 tokens of well-organized garbage. The compiler sits upstream of everything here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefix cache invalidation downstream.&lt;/strong&gt; TokenPilot's authors found that mutating the prompt sequence to save tokens invalidates the KV prefix cache, and the misses can cost more than the tokens saved. Assemble a stable prefix and vary the tail.&lt;/p&gt;

&lt;p&gt;One related trap: this is not token-level compression. The AGORA authors tested extractive token-level compressors across 17 agent configurations and reported that all 17 collapsed despite achieving real compression, because the compression broke action grammar. Dropping tokens inside a well-formed fact is a different operation from dropping a whole low-ranking fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;Log the token estimate and memory count on the context calls you already make. Then run one subject at three budgets and find where the answers stop changing. If your curve flattens where mine did, you have your number in an afternoon.&lt;/p&gt;

&lt;p&gt;If you would rather not build the compilation and ranking layer, the runtime those numbers came from is &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;Apache-2.0 and on GitHub&lt;/a&gt;. It boots against your own Postgres with one command. The &lt;a href="https://www.statewave.ai/blog/token-bounded-context-assembly" rel="noopener noreferrer"&gt;longer version of this post&lt;/a&gt; carries the full signal list and the receipt model I skipped.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>AI agent memory vs RAG — what's the difference?</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Fri, 11 Sep 2026 15:28:28 +0000</pubDate>
      <link>https://dev.to/statewave/ai-agent-memory-vs-rag-whats-the-difference-17cc</link>
      <guid>https://dev.to/statewave/ai-agent-memory-vs-rag-whats-the-difference-17cc</guid>
      <description>&lt;p&gt;Most teams building on LLMs end up with two patterns in the same codebase: &lt;strong&gt;RAG&lt;/strong&gt; for looking things up in a corpus, and some hand-rolled &lt;strong&gt;memory&lt;/strong&gt; for remembering what the agent has done or what the user has said. The two are often confused, and the confusion costs real engineering time when one is used in place of the other.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;RAG retrieves &lt;em&gt;content the agent doesn't already know&lt;/em&gt;. Memory retrieves &lt;em&gt;context the agent has already participated in&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They share a vector store but they answer different questions, store different shapes of data, and have different correctness requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared substrate
&lt;/h2&gt;

&lt;p&gt;Both patterns sit on top of an embedding store — usually pgvector or a dedicated vector database — and both use approximate-nearest-neighbour search at retrieval time. That's where the overlap ends.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG&lt;/strong&gt; stores chunks of source documents (markdown pages, PDFs, support articles). Retrieval returns &lt;em&gt;the most semantically similar chunks&lt;/em&gt; to the user's question, which are spliced into the prompt as grounding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; stores typed records of what happened: episodes (raw conversation turns, tool calls, decisions) and compiled memories (typed facts derived from episodes, with provenance back to the source). Retrieval returns &lt;em&gt;the most relevant prior context for this subject&lt;/em&gt;, ranked by recency, kind, validity, and similarity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only need to ground answers in static documentation, you need RAG. If you need the agent to &lt;em&gt;remember the user across sessions&lt;/em&gt;, you need memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where RAG breaks down
&lt;/h2&gt;

&lt;p&gt;RAG was designed to answer "what does our documentation say about X?" — not "what did this user tell us last month?" When teams stretch RAG to cover memory, three failure modes show up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Embedding-nearest is not decision-relevant.&lt;/strong&gt; A user message "I'm allergic to peanuts" and a later question "What should I order for lunch?" don't have close embeddings. Cosine similarity will surface every restaurant chunk before the allergy note. Memory ranking needs more than similarity — it needs kind priority, temporal validity, and explicit provenance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no compaction.&lt;/strong&gt; RAG indexes the corpus as-is. Memory has to &lt;em&gt;summarize&lt;/em&gt; — turning 200 conversation turns into "user is a senior engineer at a fintech, prefers terse responses" — because the prompt budget is finite. RAG doesn't do that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No invalidation model.&lt;/strong&gt; If the user's job changes, the old "works at a fintech" record needs to be marked superseded, not retrieved alongside the new one. RAG's append-only chunk store has no concept of validity windows or supersession.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can paper over each of these in your application code. Teams that do end up with a memory runtime in everything but name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where memory goes beyond retrieval
&lt;/h2&gt;

&lt;p&gt;A memory runtime adds three things on top of the vector store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compilation&lt;/strong&gt; — a pass over raw episodes that produces typed memories — profile facts, preferences, procedures, episode summaries — with confidence scores and validity windows. This is what shrinks 200 turns into one fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic ranking&lt;/strong&gt; — scoring that mixes similarity with kind priority (a procedure beats a casual mention), recency, temporal validity, and an explicit token budget. Same query → same bundle. No silent re-ordering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provenance&lt;/strong&gt; — every compiled memory carries the IDs of the episodes it was derived from. When an agent answers from memory, the answer is auditable back to the raw event that produced it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those are properties of "RAG" in the literature sense. They're what makes memory infrastructure rather than retrieval over chat logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for which
&lt;/h2&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;RAG&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Question shape&lt;/td&gt;
&lt;td&gt;"What does our content say about X?"&lt;/td&gt;
&lt;td&gt;"What does this user / agent / project need to know right now?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data shape&lt;/td&gt;
&lt;td&gt;Document chunks&lt;/td&gt;
&lt;td&gt;Episodes → compiled memories with provenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ranking signal&lt;/td&gt;
&lt;td&gt;Cosine similarity&lt;/td&gt;
&lt;td&gt;Similarity + kind priority + recency + validity + token budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutability&lt;/td&gt;
&lt;td&gt;Append-only chunks; reindex on doc update&lt;/td&gt;
&lt;td&gt;Episodes append-only; memories supersede; compaction is idempotent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;Top-K chunks&lt;/td&gt;
&lt;td&gt;Token-bounded bundle ready to drop into a prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most production agents need both. The grounding corpus (docs, knowledge base) lives in RAG. The user / account / project context lives in memory.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Trying to make either pattern do the other's job is the common architecture mistake — and it's the one we built Statewave to stop people from making.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Statewave is in this picture
&lt;/h2&gt;

&lt;p&gt;Statewave is the memory layer — episodes in, ranked context bundles out, with deterministic ranking and provenance. It uses pgvector under the hood (no separate vector DB to operate) but it's not a RAG framework: there's no document loader, no chunker, no retriever interface for grounding-over-corpora. If you want RAG, plug Statewave alongside your existing RAG stack — Statewave handles the &lt;em&gt;who you're talking to&lt;/em&gt; layer, your RAG framework handles the &lt;em&gt;what does the knowledge base say&lt;/em&gt; layer.&lt;/p&gt;

&lt;p&gt;The architecture page on the &lt;a href="https://github.com/smaramwbc/statewave-docs/blob/main/architecture/overview.md" rel="noopener noreferrer"&gt;docs site&lt;/a&gt; goes deeper on the ranking signals and the compile-vs-retrieve split. The &lt;a href="https://github.com/smaramwbc/statewave-docs/blob/main/getting-started.md" rel="noopener noreferrer"&gt;getting-started guide&lt;/a&gt; is a five-minute Docker Compose path if you want to try it side-by-side with your current RAG setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. What's the difference between RAG and AI agent memory?
&lt;/h3&gt;

&lt;p&gt;RAG retrieves content the agent doesn't already know — document chunks ranked by cosine similarity. Memory retrieves context the agent has already participated in — episodes and compiled facts ranked by recency, kind, validity, and similarity.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Can I use RAG in place of a memory layer for agent memory?
&lt;/h3&gt;

&lt;p&gt;You can stretch it, but three failure modes show up: embedding-nearest isn't decision-relevant (an allergy note won't be the closest embedding to a lunch question), there's no compaction of history into durable facts, and there's no invalidation model for facts that get superseded.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Do I need both RAG and a memory runtime?
&lt;/h3&gt;

&lt;p&gt;Most production agents do. The grounding corpus — docs, knowledge base — lives in RAG. The user, account, or project context lives in memory. Trying to make either pattern do the other's job is the common architecture mistake.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. What does a memory runtime add that a vector store alone doesn't?
&lt;/h3&gt;

&lt;p&gt;Three things: compilation (turning raw episodes into typed facts with confidence and validity), deterministic ranking (the same query always returns the same bundle), and provenance (every compiled memory carries the IDs of the episodes it came from).&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Is Statewave a RAG framework?
&lt;/h3&gt;

&lt;p&gt;No. It uses pgvector under the hood but ships no document loader, chunker, or retriever for grounding over a corpus. It's the who-you're-talking-to layer, meant to run alongside your existing RAG stack rather than replace it.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. How do I decide which one to reach for?
&lt;/h3&gt;

&lt;p&gt;Look at the shape of the question. "What does our content say about X?" is RAG. "What does this user, agent, or project need to know right now?" is memory.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.statewave.ai/blog/ai-agent-memory-vs-rag" rel="noopener noreferrer"&gt;Statewave blog&lt;/a&gt;. Statewave is an open-source, self-hosted memory runtime for AI agents — &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
      <category>architecture</category>
    </item>
    <item>
      <title>8 Open Source Tools for Building AI Agents with Memory</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Thu, 03 Sep 2026 14:09:06 +0000</pubDate>
      <link>https://dev.to/statewave/8-open-source-tools-for-building-ai-agents-with-memory-4i47</link>
      <guid>https://dev.to/statewave/8-open-source-tools-for-building-ai-agents-with-memory-4i47</guid>
      <description>&lt;p&gt;An agent that remembers needs four things: somewhere to put raw events, something that turns them into facts, a way to retrieve those facts under a token budget, and a framework to run the loop. No single tool does all four well, which is why this list is eight tools and not one.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;Statewave&lt;/a&gt;, an open-source memory runtime, so treat the entry about us accordingly. Everything below is Apache-2.0, MIT, or the PostgreSQL License, verified from the LICENSE file rather than the badge, checked on 1 September 2026. MCP is mid-transition from MIT to Apache-2.0.&lt;/p&gt;

&lt;p&gt;Grouping below is by what each tool owns, because the most common mistake in this category is picking two tools that own the same layer and none that own the missing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The memory layer&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Statewave&lt;/strong&gt;
&lt;/h3&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%2Flw2nx4c17xmhmm15d4mv.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%2Flw2nx4c17xmhmm15d4mv.png" alt="Statewave" width="799" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A memory runtime that sits behind your agents over HTTP. You send raw events as episodes; it compiles them into typed memories with confidence scores and validity windows, and you request a ranked, token-bounded bundle when you need context.&lt;/p&gt;

&lt;p&gt;from statewave import StatewaveClient  &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;with StatewaveClient("&lt;a href="http://localhost:8100%22" rel="noopener noreferrer"&gt;http://localhost:8100"&lt;/a&gt;) as sw:     sw.create_episode(subject_id="user-42", source="chat", type="message",                       payload={"text": "Alice asked about pricing tiers"})   sw.compile_memories("user-42")    print(sw.get_context("user-42", task="answer pricing", max_tokens=1000).assembled_context)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What it owns: storage, consolidation, ranked retrieval, conflict resolution, provenance. What it deliberately does not own: your agent loop. Storage is Postgres with pgvector and no separate vector service. &lt;strong&gt;npx &lt;a class="mentioned-user" href="https://dev.to/statewavedev"&gt;@statewavedev&lt;/a&gt;/statewave&lt;/strong&gt; boots the API, admin console, and Postgres locally. The server defaults to demo mode with stub hash-based embeddings and the heuristic compiler, which means no real semantic search but a working loop.&lt;/p&gt;

&lt;p&gt;Determinism is the property we care most about. The same subject, task, and token budget return the same bytes, which is what makes retrieval regression-testable.&lt;/p&gt;

&lt;p&gt;Where it costs you: cross-subject retrieval is two calls, and the default compiler is heuristic regex. Switch to &lt;strong&gt;STATEWAVE_COMPILER_TYPE=llm&lt;/strong&gt; for better extraction from messy conversation, or keep the heuristic compiler deliberately if no customer text may leave your network.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Mem0&lt;/strong&gt;
&lt;/h3&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%2Flgk770j85xavzl98sap9.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%2Flgk770j85xavzl98sap9.png" alt="Mem0" width="799" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mem0ai/mem0" rel="noopener noreferrer"&gt;Mem0&lt;/a&gt; is the shortest path from nothing to an assistant that remembers a user. Its API is four calls wide: add, search, update, delete. Apache-2.0 for the library, with a managed platform sold separately.&lt;/p&gt;

&lt;p&gt;Pick it when time to first working result matters more than being able to explain a specific retrieval later. Composition of its vector store, graph layer, and reranker is not broken down in public docs, so auditability means reading source.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Graphiti&lt;/strong&gt;
&lt;/h3&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%2Fv1e5iv7ijsqge5hy4ra5.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%2Fv1e5iv7ijsqge5hy4ra5.png" alt="Graphiti" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/getzep/graphiti" rel="noopener noreferrer"&gt;Graphiti&lt;/a&gt; is a temporal knowledge graph that tracks when a fact became valid and when it stopped being true. That is a real answer to stale memory, which most fact stores handle by overwriting and hoping.&lt;/p&gt;

&lt;p&gt;A note that saves evaluation time: many lists point to &lt;a href="https://github.com/getzep/zep" rel="noopener noreferrer"&gt;getzep/zep&lt;/a&gt; for this project. That repo’s README describes itself as examples for the managed Zep Cloud rather than the product, checked 1 September 2026. Graphiti is the self-hostable piece.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Cognee&lt;/strong&gt;
&lt;/h3&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%2F5oaf1spw91s3tnpefevk.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%2F5oaf1spw91s3tnpefevk.png" alt="Cognee" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;Cognee&lt;/a&gt; builds a self-hosted knowledge graph with ontology grounding, combining embeddings with graph reasoning. Apache-2.0.&lt;/p&gt;

&lt;p&gt;Reach for it when the relationships between entities carry the meaning, not just the facts about each one. It is more machinery than a preference store needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The orchestration layer&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. LangGraph&lt;/strong&gt;
&lt;/h3&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%2Fjp2nha9i1rpl377evea4.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%2Fjp2nha9i1rpl377evea4.png" alt="LangGraph" width="799" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/langchain-ai/langgraph" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt; models agents as state machines with explicit nodes and edges, which makes multi-step flows debuggable in a way that a while-loop over tool calls is not. MIT.&lt;/p&gt;

&lt;p&gt;Its checkpointer handles per-thread state well. Worth being precise about the boundary: thread-level persistence is not the same as cross-session, cross-agent memory, and conflating them is how teams end up with an agent that remembers a conversation but not a customer.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;6. Letta&lt;/strong&gt;
&lt;/h3&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%2F1yzdan7qy4sst6vlpyod.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%2F1yzdan7qy4sst6vlpyod.png" alt="Letta" width="799" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/letta-ai/letta" rel="noopener noreferrer"&gt;Letta&lt;/a&gt; carries the MemGPT lineage and is a stateful agent runtime rather than a memory layer. It owns the reasoning loop, tool calls, and context management. Apache-2.0.&lt;/p&gt;

&lt;p&gt;Check what you are adopting before you commit to it. &lt;strong&gt;letta-ai/letta&lt;/strong&gt; now describes itself as a landing page for the project, with the retired Letta V1 server preserved on an &lt;strong&gt;archive&lt;/strong&gt; branch and marked unsupported and not for production use. The current path is the hosted platform and the Letta Agent SDK, so Letta is no longer the self-hostable “one decision instead of four” it used to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The protocol and storage layers&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;7. Model Context Protocol&lt;/strong&gt;
&lt;/h3&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%2Fqx5iop6d3os7v35b0xv8.jpg" 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%2Fqx5iop6d3os7v35b0xv8.jpg" alt="Model Context Protocol" width="512" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; is the piece people skip, then rebuild badly. It is an open protocol for exposing tools and data to LLM clients, built on JSON-RPC.&lt;/p&gt;

&lt;p&gt;Why it belongs on a memory list: if four agents in three frameworks need the same memory, a protocol endpoint is what stops you writing three adapters. We expose Statewave over MCP for exactly this, so a Claude custom connector and a Python agent hit one memory service without either knowing about the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;8. pgvector&lt;/strong&gt;
&lt;/h3&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%2F79vo79vxi62alrp69dq2.jpg" 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%2F79vo79vxi62alrp69dq2.jpg" alt="pgvector" width="512" height="199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt; adds vector types and distance operators to Postgres. Not glamorous, and it removes an entire moving part from your architecture.&lt;/p&gt;

&lt;p&gt;One detail worth knowing: use an HNSW index rather than IVFFlat for anything that grows. IVFFlat recall depends on lists and probes matching your row count, so a corpus that outgrows its tuning quietly returns worse neighbors. We migrated for that reason and wrote up the details in the Postgres post.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How to assemble these&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Stacks that work: pair one tool per layer:&lt;/p&gt;

&lt;p&gt;●      &lt;strong&gt;Fast prototype:&lt;/strong&gt; LangGraph plus Mem0. Two decisions, working today.&lt;br&gt;&lt;br&gt;
●      &lt;strong&gt;Self-hosted with audit requirements:&lt;/strong&gt; &lt;a href="https://www.statewave.ai/blog/self-hosting-ai-memory" rel="noopener noreferrer"&gt;your framework plus Statewave on Postgres and pgvector, exposed over MCP&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
●      &lt;strong&gt;Relationship-heavy domains:&lt;/strong&gt; Graphiti or Cognee for memory, LangGraph for orchestration.&lt;/p&gt;

&lt;p&gt;A common anti-pattern is picking Letta and Mem0 together, or LangGraph checkpointers and expecting cross-session memory. Both are two tools fighting over one layer while a different layer stays empty.&lt;/p&gt;

&lt;p&gt;If you want the working code rather than the list, we keep three runnable demos: &lt;a href="https://github.com/smaramwbc/statewave-multi-agent-memory" rel="noopener noreferrer"&gt;multi-agent memory&lt;/a&gt; with conflicting sources and automatic supersession, &lt;a href="https://github.com/smaramwbc/statewave-multi-agent-shared-context" rel="noopener noreferrer"&gt;multi-agent shared context&lt;/a&gt; where a planner and coder stop contradicting each other, and a &lt;a href="https://github.com/smaramwbc/statewave-personal-assistant" rel="noopener noreferrer"&gt;personal assistant&lt;/a&gt; that boots in five minutes without an LLM key.&lt;/p&gt;

&lt;p&gt;No neutral, third-party benchmark compares these on the same task with the same corpus. Ours covers a subset and we ran it, which is exactly why it is not the one to settle your decision. Run the eval on your own data before committing to any of them.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>opensource</category>
      <category>tools</category>
    </item>
    <item>
      <title>7 Postgres Tools Every AI Engineer Should Know</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Thu, 03 Sep 2026 13:59:42 +0000</pubDate>
      <link>https://dev.to/statewave/7-postgres-tools-every-ai-engineer-should-know-2noj</link>
      <guid>https://dev.to/statewave/7-postgres-tools-every-ai-engineer-should-know-2noj</guid>
      <description>&lt;p&gt;Most AI stacks add a vector database on day one and a second datastore to keep in sync forever after. A short list of Postgres extensions removes that decision for a large class of workloads.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;Statewave&lt;/a&gt;, an open-source memory runtime for AI agents that runs on Postgres and nothing else. No separate vector service. That constraint forced us to learn which extensions genuinely carry AI workloads and which are resume padding. Below are seven, ordered by how often they earn their install.&lt;/p&gt;

&lt;p&gt;Our own dependency list is one extension long, which is the first useful signal in this post: &lt;strong&gt;vector is the only one we require. Two entries below are not extensions at all, full-text search ships in Postgres core, and PgBouncer is a connection pooler, but both earn their place on an AI workload, so they are here.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. pgvector&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt; is what makes the rest of the argument possible. It adds a vector column type plus distance operators for cosine, L2, and inner product.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CREATE EXTENSION IF NOT EXISTS vector; ALTER TABLE memories ALTER COLUMN embedding TYPE vector(1536);&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Index choice is the part worth internalizing, because we shipped the wrong one first. Our initial migration built an IVFFlat index:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CREATE INDEX ON memories USING ivfflat (embedding vector_cosine_ops);&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/pgvector/pgvector#ivfflat" rel="noopener noreferrer"&gt;IVFFlat&lt;/a&gt; partitions vectors into lists and probes a subset at query time. It builds fast and uses little memory. Recall, though, depends on lists and probes being tuned against your actual row count, and a corpus that grows past what you tuned for silently returns worse neighbors. Nothing errors. Results just get quietly less relevant.&lt;/p&gt;

&lt;p&gt;We moved to HNSW:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CREATE INDEX ix_memories_embedding   ON memories USING hnsw (embedding vector_cosine_ops);&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/pgvector/pgvector#hnsw" rel="noopener noreferrer"&gt;HNSW&lt;/a&gt; builds a navigable graph. Better recall at the same latency, stable as the corpus grows, and no list-count tuning. Cost is real: build time and memory. On a small corpus the migration finishes in under a second, but HNSW build is the slow step at scale, so our migration sets statement_timeout = '20min' to survive large tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use IVFFlat&lt;/strong&gt; when your corpus is static and you need a fast build. &lt;strong&gt;Use HNSW&lt;/strong&gt; for anything that grows. If you inherited an IVFFlat index and nobody has retuned lists since the table doubled, that is worth checking today.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Built-in tsvector and GIN (no extension required)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Semantic search alone fails on exact tokens. A user asking for error code SW-4021 needs lexical matching, and embeddings will happily return five semantically adjacent errors instead.&lt;/p&gt;

&lt;p&gt;Postgres ships &lt;a href="https://www.postgresql.org/docs/current/textsearch.html" rel="noopener noreferrer"&gt;full-text search&lt;/a&gt; in core. A generated column plus a GIN index gives you lexical retrieval with zero dependencies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ALTER TABLE memories   ADD COLUMN content_tsvector tsvector   GENERATED ALWAYS AS (to_tsvector('english', content)) STORED; CREATE INDEX ix_memories_content_tsvector   ON memories USING gin (content_tsvector);&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GENERATED ALWAYS AS ... STORED means Postgres maintains the column on every write. No trigger to forget, no backfill job to schedule.&lt;/p&gt;

&lt;p&gt;We run this alongside vector search and fuse the two, which is what "hybrid retrieval" means in practice. Including it here because half the teams that install a vector database do so to solve a problem that lexical search solves better.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. pgcrypto&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Small but load-bearing. &lt;a href="https://www.postgresql.org/docs/current/pgcrypto.html" rel="noopener noreferrer"&gt;pgcrypto&lt;/a&gt; supplies gen_random_uuid() for primary keys generated server-side, so you are not round-tripping to the application for an ID.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CREATE EXTENSION IF NOT EXISTS pgcrypto;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On Postgres 13 and newer, gen_random_uuid() is in core and you may not need the extension at all. Check your version before adding it. Its digest and encryption functions matter separately if you are storing anything sensitive in a memory layer, which for agent memory you usually are.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. pg_trgm&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/pgtrgm.html" rel="noopener noreferrer"&gt;pg_trgm&lt;/a&gt; does trigram matching for fuzzy string comparison. Entity resolution is the AI-specific use: deciding that "Acme Corp", "ACME Corporation", and "acme corp." are the same subject before you write three separate memory rows about them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX ON subjects USING gin (name gin_trgm_ops); SELECT name, similarity(name, 'acme corp') AS score FROM subjects WHERE name % 'acme corp' ORDER BY score DESC;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Embeddings are the wrong tool here. Two spellings of the same company name are lexically close and semantically identical, so cosine similarity gives you no separation between the right match and every other company in your table.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. pg_stat_statements&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/pgstatstatements.html" rel="noopener noreferrer"&gt;pg_stat_statements&lt;/a&gt; tells you why your agent feels slow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CREATE EXTENSION IF NOT EXISTS pg_stat_statements; SELECT query, calls, mean_exec_time, total_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AI workloads have a specific failure shape: one vector query looks fine at 40ms, then you discover the agent issues it eleven times per turn. Mean latency stays healthy while total time balloons. total_exec_time catches that; a p99 dashboard does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;6. pg_cron&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/citusdata/pg_cron" rel="noopener noreferrer"&gt;pg_cron&lt;/a&gt; covers the background work agent memory needs. Compiling raw events into typed facts, expiring memories past their validity window, and recomputing scores are all jobs that should not run on the request path.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CREATE EXTENSION IF NOT EXISTS pg_cron; SELECT cron.schedule('expire-memories', '0 * * * *',   $$UPDATE memories SET status='expired'    WHERE valid_until &amp;lt; now() AND status='active'$$);&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We run compilation as durable jobs in the application layer rather than in pg_cron, because our jobs call an LLM and need retry semantics Postgres should not own. Honest rule: pg_cron is right for deterministic SQL maintenance and wrong for anything that makes a network call.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;7. PgBouncer (a pooler, not an extension)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Included deliberately, because the thing that breaks first when you scale an AI service is not the vector index. It is connection count.&lt;/p&gt;

&lt;p&gt;Each API replica holds a pool. Ten replicas at fifteen connections each is 150 logical connections crowding max_connections, and Postgres connection overhead is not free. Raising max_connections postpones the problem and makes it worse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pgbouncer.org/" rel="noopener noreferrer"&gt;PgBouncer&lt;/a&gt; in &lt;strong&gt;transaction mode&lt;/strong&gt; decouples logical client connections from physical backends: a pool of 30 to 60 backend connections comfortably serves 5 to 15 replicas. Transaction mode is the right setting when sessions are short. If you use prepared statements, PgBouncer 1.21 and later support them in transaction mode via &lt;strong&gt;max_prepared_statements&lt;/strong&gt;; on older versions, avoid holding them across transactions.&lt;/p&gt;

&lt;p&gt;One thing to know before you switch: &lt;strong&gt;pg_stat_activity&lt;/strong&gt; then shows &lt;strong&gt;PgBouncer's&lt;/strong&gt; identity rather than your replicas. Use &lt;strong&gt;SHOW POOLS&lt;/strong&gt; and &lt;strong&gt;SHOW CLIENTS&lt;/strong&gt; for the per-replica picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What we would actually install&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For a new AI service on Postgres, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;vector&lt;/strong&gt; with an HNSW index. Non-negotiable.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in tsvector plus GIN.&lt;/strong&gt; Free, and it fixes the exact-token failures embeddings cannot.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pg_stat_statements.&lt;/strong&gt; Install before you need it, because the query pattern you need to diagnose is one you have to catch in the act.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pg_trgm&lt;/strong&gt; once you have real user-entered entity names.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PgBouncer&lt;/strong&gt; at the point where replica count times pool size approaches &lt;strong&gt;max_connections.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;pgcrypto&lt;/strong&gt; and &lt;strong&gt;pg_cron&lt;/strong&gt; are situational. Check your Postgres version for the first and your job semantics for the second.&lt;/p&gt;

&lt;p&gt;Broader point: &lt;a href="https://www.statewave.ai/blog/self-hosting-ai-memory" rel="noopener noreferrer"&gt;a memory layer for AI agents can run on Postgres with one extension and no separate vector service&lt;/a&gt;. We wrote up the &lt;a href="https://www.statewave.ai/blog/self-hosted-memory-postgres-pgvector" rel="noopener noreferrer"&gt;storage decisions in more detail&lt;/a&gt;, and the IVFFlat-to-HNSW migration is readable in the repo linked at the top if you would rather check it than take our word for it.&lt;/p&gt;

&lt;p&gt;What did we miss? If you are running something in production that earned its place on an AI workload, name it below.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
  </channel>
</rss>
