<?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>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>
    <item>
      <title>6 Ways to Add Observability to Your AI Agent Pipeline</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Thu, 03 Sep 2026 13:57:44 +0000</pubDate>
      <link>https://dev.to/statewave/6-ways-to-add-observability-to-your-ai-agent-pipeline-36hn</link>
      <guid>https://dev.to/statewave/6-ways-to-add-observability-to-your-ai-agent-pipeline-36hn</guid>
      <description>&lt;p&gt;When an agent gives a wrong answer, the prompt log tells you what it was asked and what it said. It does not tell you which stored facts reached the prompt, which were filtered out, or which were stale. That gap is where agent debugging actually lives.&lt;/p&gt;

&lt;p&gt;We build &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;Statewave&lt;/a&gt;, a memory runtime for AI agents, so most of what follows comes from instrumenting the retrieval side of that problem. Six layers, ordered from the one everyone already has to the one almost nobody does.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;1. Request tracing across the hop boundary&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Start here because it is cheap and everything else attaches to it. An agent turn is rarely one process: the framework calls a model, the model requests a tool, the tool calls a memory service, the memory service queries Postgres.&lt;/p&gt;

&lt;p&gt;Propagate a request ID across every hop and log it at each. Without it you have four logs and no way to join them, which turns a five-minute investigation into an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; is the standard worth adopting rather than inventing a correlation header. The span boundaries that matter for agents are: turn, model call, tool call, retrieval.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Deterministic retrieval, so runs are comparable&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This one is a design decision rather than a tool, and it gates the usefulness of everything after it.&lt;/p&gt;

&lt;p&gt;If your memory layer searches at query time and samples differently per call, two identical requests return different context. You cannot compare a good run against a bad one because the inputs were never the same. Non-determinism does not just make debugging harder; it makes A/B comparison meaningless.&lt;/p&gt;

&lt;p&gt;Our approach is to move the expensive work off the query path: episodes compile into typed memories once per subject change, then &lt;a href="https://www.statewave.ai/blog/ai-agent-memory-vs-rag" rel="noopener noreferrer"&gt;context assembly ranks those compiled memories&lt;/a&gt; by kind priority, recency, task relevance, temporal validity, and semantic similarity. Same inputs, same bytes.&lt;/p&gt;

&lt;p&gt;Whatever memory layer you use, find out whether it guarantees this. If it does not, your first observability investment is making retrieval reproducible, not adding another dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. Retrieval receipts&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the layer that answers "why did the agent say that?"&lt;/p&gt;

&lt;p&gt;A receipt is an immutable record of one context assembly: which memories and episodes were selected, the content hash of the assembled bundle, and the policy snapshot in force at the time. Ours are ULID-keyed, which gives chronological sorting at the database level without a separate index.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;{   "receipt_id": "01J8XKQ2M7...",   "subject_id": "user-42",   "selected_entries": ["mem_881", "mem_902", "ep_1204"],   "context_hash": "sha256:9f2c...",   "canonicalization_version": 3,   "policy_snapshot": {"bundle_hash": "sha256:41ab..."} }&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One detail worth stealing: the &lt;strong&gt;canonicalization_version&lt;/strong&gt; field. Hashing an assembled context is only useful if you can tell later whether a hash mismatch means the content changed or the hashing routine changed. Version the canonicalization and historical hashes stay verifiable.&lt;/p&gt;

&lt;p&gt;Prompt logs cannot do this job. A prompt log shows the final string; a receipt shows the decisions that produced it, including which memories were considered and rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;4. Replay, with honest semantics&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Receipts let you re-run an assembly. What replay means, precisely, matters more than having it.&lt;/p&gt;

&lt;p&gt;Ours re-runs against &lt;strong&gt;current&lt;/strong&gt; memories using the &lt;strong&gt;original&lt;/strong&gt; policy bundle captured in the receipt. That is deliberately not byte-for-byte historical reproduction, and the difference is worth stating plainly:&lt;/p&gt;

&lt;p&gt;●      Memories may have been added, tombstoned, or superseded since. Those appear in the diff as added or removed entries.&lt;br&gt;&lt;br&gt;
●      New episodes ingested since will show up in scope.&lt;br&gt;&lt;br&gt;
●      Scoring code runs at whatever version is deployed now, so a changed heuristic shows up as a context hash change.&lt;/p&gt;

&lt;p&gt;True point-in-time reproduction needs memory snapshots, which we have not built. If you are designing this yourself, decide which semantic you need before building, because retrofitting snapshots is much harder than including them.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;5. Health scoring on the subject, not just the service&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Service health tells you Postgres is up. It does not tell you that one customer's memory has quietly degraded.&lt;/p&gt;

&lt;p&gt;Subject-level health scoring is the agent-specific version of an SLO. Ours computes a deterministic 0 to 100 score from signals already in the data: unresolved sessions, repeated issue patterns, urgency markers, idle open issues, and SLA breaches. Every factor returns its own contribution, so the score is explainable rather than a number nobody trusts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;# every penalty is named and capped, so a score decomposes _UNRESOLVED_ISSUE_PENALTY = 15   # per open session, capped at 45 _REPEATED_ISSUE_PENALTY   = 20   # 2+ sessions sharing a pattern _ESCALATION_PENALTY     = 10   # per episode with urgency markers, capped at 20 _IDLE_OPEN_PENALTY        = 15   # open issue, no activity in 7+ days _SLA_BREACH_PENALTY       = 10   # per breaching session, capped at 20&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No ML, no stored state, computed on demand. Determinism is the constraint that keeps it useful: the same data always produces the same score, so a change in the number always means a change in the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;6. Alert on state transitions, not thresholds&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Threshold alerts on a score that moves every request produce noise until someone mutes the channel.&lt;/p&gt;

&lt;p&gt;Alert on transitions between named states instead. Ours emits &lt;strong&gt;subject.health_degraded&lt;/strong&gt; when a subject moves healthy to watch, watch to at_risk, or healthy to at_risk, and &lt;strong&gt;subject.health_improved&lt;/strong&gt; on the way back. Unchanged states emit nothing, because the deduplication compares against the last cached state.&lt;/p&gt;

&lt;p&gt;That single rule, transitions rather than levels, is the difference between an alert channel people read and one they filter.&lt;/p&gt;

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

&lt;p&gt;If you have none of this today, the order that pays fastest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Request IDs across hops.&lt;/strong&gt; One afternoon, and it makes every later layer joinable.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make retrieval deterministic.&lt;/strong&gt; Nothing downstream is trustworthy without it.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Receipts on retrieval.&lt;/strong&gt; The single highest-value artifact for debugging agents, and the one most stacks lack.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transition-based alerts.&lt;/strong&gt; Cheap once you have a state model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Replay and subject health are worth building when you have compliance requirements or a support workflow, and skippable if you do not.&lt;/p&gt;

&lt;p&gt;Our implementations of the above are Apache-2.0 and readable in the &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;repo&lt;/a&gt; if you want the receipt schema or the health scoring rather than the summary. The &lt;a href="https://www.statewave.ai/blog/agent-memory-provenance-audit-trails" rel="noopener noreferrer"&gt;provenance and audit trail write-up&lt;/a&gt; covers the reasoning behind layer three in more depth.&lt;/p&gt;

&lt;p&gt;What does your agent observability stack look like? Particularly curious whether anyone has solved true point-in-time replay without snapshotting the whole store.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>debugging</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Best Open-Source AI Agent Memory Tools (Ranked by What They Guarantee)</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Mon, 17 Aug 2026 06:37:25 +0000</pubDate>
      <link>https://dev.to/statewave/best-open-source-ai-agent-memory-tools-ranked-by-what-they-guarantee-451g</link>
      <guid>https://dev.to/statewave/best-open-source-ai-agent-memory-tools-ranked-by-what-they-guarantee-451g</guid>
      <description>&lt;p&gt;The best open-source AI agent memory tool is the one that guarantees the properties your system depends on: a self-hostable runtime, deterministic retrieval, provenance back to source events, and a permissive license. Ranked on those four, Statewave leads, ahead of Cognee, Mem0, Graphiti, and Letta.&lt;/p&gt;

&lt;p&gt;We checked the LICENSE file and README of every project below, reading the repository rather than the landing page. That produced the finding this post is built on: &lt;strong&gt;two of the four alternatives most often listed as self-hostable memory servers do not ship that from the repo people link to.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post ranks by capability, not by adoption. Adoption tells you what other teams picked last year. Capability tells you what will hold when your agent is in production.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: we build Statewave. Every claim below about another project is quoted from or linked to that project's public repository, so you can check each one yourself.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What makes an agent memory tool "best"?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Four properties separate a memory runtime from a fact store with a vector index bolted on. Every ranking below is scored against these.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hostable as the actual product.&lt;/strong&gt; Not a client library that calls someone else's cloud. When data cannot leave your infrastructure, this is the first filter, and it eliminates more tools than teams expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic retrieval.&lt;/strong&gt; The same subject, task, and token budget returns the same context. Without this, you cannot regression-test memory, and you cannot explain why two identical requests produced different answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provenance.&lt;/strong&gt; Every compiled fact traces back to the raw event that produced it. When someone asks why the agent said something about their account, that has to be a query rather than an investigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissive license.&lt;/strong&gt; Apache-2.0 or MIT, with no source-disclosure obligation and no seat-count trap. All five here pass this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Which open-source memory tools are worth shortlisting?&lt;/strong&gt;
&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;Self-hosted product&lt;/th&gt;
&lt;th&gt;Deterministic retrieval&lt;/th&gt;
&lt;th&gt;Provenance&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Statewave&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, single service&lt;/td&gt;
&lt;td&gt;Yes, documented contract&lt;/td&gt;
&lt;td&gt;Yes, per memory&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cognee&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not advertised&lt;/td&gt;
&lt;td&gt;Graph lineage&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mem0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Library and self-hosted server; managed platform separate&lt;/td&gt;
&lt;td&gt;Not advertised&lt;/td&gt;
&lt;td&gt;Not surfaced in public docs&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Graphiti&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, library plus a REST/MCP server&lt;/td&gt;
&lt;td&gt;Not advertised&lt;/td&gt;
&lt;td&gt;Edge validity dates&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Letta&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Agent runtime, not a memory layer&lt;/td&gt;
&lt;td&gt;Not advertised&lt;/td&gt;
&lt;td&gt;Not surfaced in public docs&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fitv5j5efc3sfiy3nlf8s.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%2Fitv5j5efc3sfiy3nlf8s.png" alt="Statewave" width="799" height="312"&gt;&lt;/a&gt;&lt;br&gt;
Statewave is a memory runtime rather than a memory library. It records raw events as append-only episodes, compiles them into typed memories with confidence scores and validity windows, then assembles a ranked, token-bounded context bundle on demand. Start the server first with npx &lt;a class="mentioned-user" href="https://dev.to/statewavedev"&gt;@statewavedev&lt;/a&gt;/statewave, then talk to it from the Python SDK (pip install statewave), which is a client rather than the runtime:&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;It leads on all four criteria for one architectural reason: the expensive work happens at compile time, not query time. The same (subject_id, task, max_tokens) returns the same bytes against the same compiled state, which is what makes retrieval regression-testable. Every memory returns source_episode_ids, so provenance is a field rather than a feature request. Storage is Postgres with pgvector and nothing else, so self-hosting is one command rather than an infrastructure project: npx &lt;a class="mentioned-user" href="https://dev.to/statewavedev"&gt;@statewavedev&lt;/a&gt;/statewave, or git clone plus docker compose up -d if you want the compose file in your own tree.&lt;/p&gt;

&lt;p&gt;Where it costs you: compiled bundles are denser than plain fact-store lookups, so a single-hop query spends more tokens than it would against a flat key-value store. For workloads that are all single-hop and cost-sensitive, that trade is not worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that must self-host and must be able to prove why a fact surfaced. Read the &lt;a href="https://www.statewave.ai/blog/agent-memory-provenance-audit-trails" rel="noopener noreferrer"&gt;ranked retrieval and provenance model&lt;/a&gt; for how the audit chain works.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. 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%2F2comicq9nzswdkzbwoug.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%2F2comicq9nzswdkzbwoug.png" alt="Cognee" width="800" height="317"&gt;&lt;/a&gt;&lt;br&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 vector embeddings with graph reasoning. It ranks second because it genuinely self-hosts and the graph gives you real lineage between entities.&lt;/p&gt;

&lt;p&gt;It ranks below Statewave on determinism, which is not advertised as a contract, and on retrieval shape: graph traversal answers "how is Alice connected to Acme" better than "what is the ranked context for Alice under a 1,000-token budget."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fits&lt;/strong&gt; domains where the relationships between entities are the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. 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%2Fiffw28m3eai6821o77xi.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%2Fiffw28m3eai6821o77xi.png" alt="Mem0" width="799" height="303"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/mem0ai/mem0" rel="noopener noreferrer"&gt;Mem0&lt;/a&gt; offers the shortest path from zero to an assistant that remembers a user, with an &lt;strong&gt;add / search / update / delete&lt;/strong&gt; surface that is four calls wide. The open-source library is Apache-2.0.&lt;/p&gt;

&lt;p&gt;It ranks third on our criteria rather than on quality, and not because it is library-only, which it no longer is: the repository ships a self-hosted Docker server with a dashboard, per-user API keys and a request audit log alongside the pip-installable library. The split that remains is between that open-source stack and the managed platform, and the README is explicit that its published scores "reflect Mem0's managed platform, which includes proprietary optimizations not available in the open-source SDK." The load-bearing gap for our criteria is retrieval composition: the public documentation still does not break down how the vector store, graph layer and reranker compose at retrieval time. If you need to explain a specific retrieval to an auditor, you will be reading source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fits&lt;/strong&gt; consumer-facing personalization where speed to first result matters more than explainability.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. 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%2Fyqyedmugyumz45rjygff.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%2Fyqyedmugyumz45rjygff.png" alt="Graphiti" width="800" height="320"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/getzep/graphiti" rel="noopener noreferrer"&gt;Graphiti&lt;/a&gt; is a temporal context graph engine that tracks when facts became valid and invalid, which is a real answer to the stale-memory problem.&lt;/p&gt;

&lt;p&gt;The ranking note here is the one that surprised us. The repository most lists point to for this project is &lt;a href="https://github.com/getzep/zep" rel="noopener noreferrer"&gt;getzep/zep&lt;/a&gt;, and its own README states it is &lt;strong&gt;not&lt;/strong&gt; the product: it holds example code and integrations for Zep Cloud, which is managed. Graphiti is the self-hostable open-source component, and it is more than a library: the repository ships a FastAPI REST service under server/ and an MCP server under mcp_server/, with a prebuilt zepai/graphiti image published on each graphiti-core release. A team whose requirement is "runs in our VPC" does not have to write that service. What it still writes is the retrieval contract, because what Graphiti returns is graph queries rather than a ranked, token-bounded, deterministic bundle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fits&lt;/strong&gt; teams that want temporal graph semantics and are willing to build the retrieval contract themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. 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%2F64zcnzo4kn0z4tsgjvd9.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%2F64zcnzo4kn0z4tsgjvd9.png" alt="Letta" width="799" height="316"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/letta-ai/letta" rel="noopener noreferrer"&gt;Letta&lt;/a&gt; carries the MemGPT lineage and is the most capable system here at the thing it actually does, which is running agents. It ranks last only because it is answering a different question: it is an agent runtime that owns the reasoning loop, the tool calls and the context management.&lt;/p&gt;

&lt;p&gt;The second repository note: that README states the repo is the legacy server and active development has moved to the Letta Agent repo. Adopting Letta means replacing your orchestration, not adding memory to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fits&lt;/strong&gt; teams with no agent stack yet who want the whole thing in one decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Which one should you pick?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Pick by the constraint that is load-bearing for you, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data cannot leave your infrastructure.&lt;/strong&gt; Statewave, Cognee, or Graphiti. Mem0's open-source stack qualifies; its managed platform does not.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to explain retrievals after the fact.&lt;/strong&gt; Statewave. Provenance and deterministic ranking are the two properties that make this answerable.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships between entities are the product.&lt;/strong&gt; Cognee or Graphiti.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have no agent framework yet.&lt;/strong&gt; Letta.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want something working this afternoon.&lt;/strong&gt; Mem0.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One honest note on all of the above, including ours: no neutral, third-party benchmark covers all five on the same task with the same corpus. Every comparison in this category, this one included, is vendor-shaped. Run the eval on your own data before committing.&lt;/p&gt;

&lt;p&gt;When the first two constraints are the binding ones, &lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;Statewave is Apache-2.0 and boots with one command&lt;/a&gt;: &lt;strong&gt;npx &lt;a class="mentioned-user" href="https://dev.to/statewavedev"&gt;@statewavedev&lt;/a&gt;/statewave.&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Best Memory Layer for Multi-Agent AI Systems With Shared Context</title>
      <dc:creator>Statewave</dc:creator>
      <pubDate>Mon, 17 Aug 2026 06:30:06 +0000</pubDate>
      <link>https://dev.to/statewave/best-memory-layer-for-multi-agent-ai-systems-with-shared-context-2900</link>
      <guid>https://dev.to/statewave/best-memory-layer-for-multi-agent-ai-systems-with-shared-context-2900</guid>
      <description>&lt;p&gt;The best memory layer for a multi-agent system is the one that keys memory to the subject rather than the agent, returns the same context to concurrent callers, enforces per-caller policy below the agents, and records which memories reached which agent. Scored on those four, Statewave leads, ahead of Mem0, LangMem, Graphiti and Letta.&lt;/p&gt;

&lt;p&gt;We went looking for one specific primitive in each of these five projects, reading their repositories and public docs rather than their landing pages. The finding: &lt;strong&gt;we could not locate a documented per-caller policy primitive in any of the four alternatives.&lt;/strong&gt; Every one of them can share memory between agents. We found no way in their public docs to say "the billing agent sees this memory and the FAQ agent does not" inside the memory layer itself.&lt;/p&gt;

&lt;p&gt;That gap is the whole subject of this post, because it is the requirement that only appears once you have more than one agent.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: we build Statewave. Every claim below about another project comes from that project's public repository or documentation, so you can check each one yourself.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why does multi-agent memory need different properties?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Single-agent memory has one reader. Multi-agent memory has several readers with different privileges, arriving concurrently, contributing facts that the others will act on. Three failures follow, and none are fixed by a bigger context window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-agent amnesia.&lt;/strong&gt; A researcher agent gathers requirements over six turns. A coder agent picks up the task with none of it, and the user re-explains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval drift between callers.&lt;/strong&gt; Two agents ask the same question about the same customer in the same minute and get different context, so they reach different conclusions from identical underlying facts. Now you are debugging a disagreement neither agent caused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uniform visibility.&lt;/strong&gt; Every agent that can read the pool can read all of it, including the contractor's coding agent and the public-facing FAQ bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What are the four requirements?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Memory keyed to the subject.&lt;/strong&gt; The unit of memory should be the entity it concerns, a customer or an account or a repository, not the agent that recorded it. Key memory by agent, and sharing becomes copying, and copies drift. Key it by session and it dies with the session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Determinism across concurrent callers.&lt;/strong&gt; The same subject, task and token budget returns the same bytes to every agent that asks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-caller policy.&lt;/strong&gt; A rule layer below the agents that decides, per request, whether a memory is passed, redacted, or denied based on who is asking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-agent audit.&lt;/strong&gt; A record of which memories entered which agent's context, under which rules, at what time. A chat transcript is not this artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Which memory layer handles shared context best?&lt;/strong&gt;
&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;Subject-keyed&lt;/th&gt;
&lt;th&gt;Deterministic&lt;/th&gt;
&lt;th&gt;Per-caller policy&lt;/th&gt;
&lt;th&gt;Cross-agent audit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Statewave&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, subjects are the primitive&lt;/td&gt;
&lt;td&gt;Yes, documented contract&lt;/td&gt;
&lt;td&gt;Yes, labels plus policy bundles&lt;/td&gt;
&lt;td&gt;Yes, state-assembly receipts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mem0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, user and agent scoping&lt;/td&gt;
&lt;td&gt;Not advertised&lt;/td&gt;
&lt;td&gt;Not found in public docs&lt;/td&gt;
&lt;td&gt;Request-level audit log, not per-memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LangMem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, store namespaces&lt;/td&gt;
&lt;td&gt;Not advertised&lt;/td&gt;
&lt;td&gt;Not found in public docs&lt;/td&gt;
&lt;td&gt;Not surfaced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Graphiti&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Graph-wide, not subject-scoped&lt;/td&gt;
&lt;td&gt;Not advertised&lt;/td&gt;
&lt;td&gt;Not found in public docs&lt;/td&gt;
&lt;td&gt;Edge validity only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Letta&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-agent by design&lt;/td&gt;
&lt;td&gt;Not advertised&lt;/td&gt;
&lt;td&gt;Not found in public docs&lt;/td&gt;
&lt;td&gt;Not surfaced&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fumoy8c6h3kmh0hubmlzi.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%2Fumoy8c6h3kmh0hubmlzi.png" alt="Statewave" width="799" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Statewave organizes everything around &lt;strong&gt;subjects&lt;/strong&gt;, the entity memory is about. Four agents reading &lt;strong&gt;customer-8821&lt;/strong&gt; see one pool of episodes and compiled facts, so sharing is the default rather than a protocol you write.&lt;/p&gt;

&lt;p&gt;Determinism comes from moving the work off the query path. Episodes compile into typed memories once per subject change, and context assembly ranks those compiled memories by kind priority, recency, task relevance, temporal validity, and semantic similarity. Same inputs, same bundle, whichever agent asks.&lt;/p&gt;

&lt;p&gt;Per-caller policy is the piece that separates it here. Memories carry sensitivity labels such as &lt;strong&gt;pii, financial,&lt;/strong&gt; and &lt;strong&gt;secret&lt;/strong&gt;. Every context request carries a caller identity. A declarative YAML policy bundle, content-hashed and immutable, decides per request whether each memory is denied, redacted, or passed. Policies run in &lt;strong&gt;log_only&lt;/strong&gt; mode first, recording what they would have filtered, before anyone enables enforcement.&lt;/p&gt;

&lt;p&gt;Cross-agent audit is the state-assembly receipt: an immutable record of which memories and episodes went into one specific bundle, hashed, with the policy snapshot attached.&lt;/p&gt;

&lt;p&gt;Where it costs you: cross-subject retrieval is not first-class. An agent needing facts about both Alice and Acme makes two calls. Subject granularity is a real design decision and getting it wrong means re-modelling later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fits&lt;/strong&gt; systems where agents have different privileges or the data is sensitive.&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%2Fddyzcdxdhopmf6wavrab.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%2Fddyzcdxdhopmf6wavrab.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; supports user, agent and session scoping, so several agents can read one user's memory pool. It ranks second because that sharing works and the API is small enough to wire up quickly.&lt;/p&gt;

&lt;p&gt;It ranks below Statewave on the second half of the list. Determinism is not advertised, and the composition of the vector store, graph layer and reranker is not broken down publicly, so two concurrent callers have no documented guarantee of identical context. We found no per-caller policy primitive: the self-hosted server issues per-user API keys, which authenticate a caller rather than decide which memories that caller may read, and Entity-Scoped Memory, which partitions the pool by user, agent, app or run, is marked platform-only. On audit it is closer than the rest of the field: the self-hosted stack keeps a request audit log. That is a record of calls, not a record of which memories entered which agent's context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fits&lt;/strong&gt; multi-agent systems where every agent is trusted equally.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. LangMem&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%2Fi2qfe5wwt0sbyrbs6ivu.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%2Fi2qfe5wwt0sbyrbs6ivu.png" alt="LangMem" width="799" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/langchain-ai/langmem" rel="noopener noreferrer"&gt;LangMem&lt;/a&gt; gives LangGraph applications a store with namespaces, which is a clean sharing boundary when every agent lives inside LangGraph.&lt;/p&gt;

&lt;p&gt;The constraint is the boundary itself. A LangGraph support agent, an OpenAI Agents SDK sales copilot and a custom Express service should share one memory surface, and the shared store reaches only the first. The package splits in two here, and the distinction is worth naming: LangMem's core primitives are storage-agnostic and travel into any application, while the shared memory that agents actually read is LangGraph's own long-term memory store. The portable half travels; the shared surface does not. Ranking, budgeting and provenance on top of the store are code you write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fits&lt;/strong&gt; teams whose agents all run inside one framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. 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%2Fzd0x0prvfyyfyojnuskv.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%2Fzd0x0prvfyyfyojnuskv.png" alt="LangMem" 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; tracks when facts became valid and invalid across a temporal context graph, which handles the stale-fact problem well when several agents write to one graph.&lt;/p&gt;

&lt;p&gt;It ranks fourth for multi-agent work because the graph is global rather than subject-scoped, so isolating one entity's context is a traversal you design. Per-caller filtering is application code above Graphiti, whether you embed the package or run its REST service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fits&lt;/strong&gt; shared-context systems where entity relationships carry the meaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. 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%2Fz4ys3juxpn0ym8i8nvf6.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%2Fz4ys3juxpn0ym8i8nvf6.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; is a stateful agent runtime with the MemGPT lineage, and it is strong at what it does. For this scorecard it ranks last because its memory model is per-agent by design: memory belongs to an agent rather than to an entity several agents share.&lt;/p&gt;

&lt;p&gt;Multi-agent sharing in that model means passing state between runtimes rather than reading one pool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fits&lt;/strong&gt; teams that want a complete agent runtime and whose agents do not need a common memory surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How do you choose?&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do your agents have different privileges?&lt;/strong&gt; If yes, per-caller policy is the binding requirement and it narrows the field to one.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do agents run in more than one framework?&lt;/strong&gt; If yes, the memory layer has to be an HTTP service, which rules out framework-native stores.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Will anyone ask which agent saw what?&lt;/strong&gt; If yes, you need receipts, not logs.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are your agents symmetric and your data non-sensitive?&lt;/strong&gt; Then requirements three and four do not bind, and Mem0 or LangMem is the faster path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Being straight about our own gaps: tenant isolation is query-scoped at the application layer rather than Postgres row-level security, rate limiting is keyed per IP, and there is no admin-action identity yet, so label promotions record when but not who. One more, since it touches the audit argument above: receipt replay re-runs the original retrieval against today's memories using the original policy bundle, which is not byte-for-byte historical reproduction. Any of those may disqualify the approach for your compliance posture.&lt;/p&gt;

&lt;p&gt;No neutral, third-party benchmark covers all five on the same multi-agent task, ours included. The scorecard above is a documentation review, and the honest way to use it is as a list of questions to ask each vendor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/smaramwbc/statewave" rel="noopener noreferrer"&gt;Statewave is Apache-2.0 and self-hosted on Postgres&lt;/a&gt;. Many agents point at one memory service over HTTP or MCP, and your orchestration stays where you built it.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
