<?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: Davis Philip</title>
    <description>The latest articles on DEV Community by Davis Philip (@davis_philip).</description>
    <link>https://dev.to/davis_philip</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%2F4190057%2F817f1c9b-644b-451e-9a8b-bcf8012ceef9.jpg</url>
      <title>DEV Community: Davis Philip</title>
      <link>https://dev.to/davis_philip</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davis_philip"/>
    <language>en</language>
    <item>
      <title>The $6,000 we couldn't take back</title>
      <dc:creator>Davis Philip</dc:creator>
      <pubDate>Sat, 17 Oct 2026 08:01:00 +0000</pubDate>
      <link>https://dev.to/davis_philip/the-6000-we-couldnt-take-back-3c8c</link>
      <guid>https://dev.to/davis_philip/the-6000-we-couldnt-take-back-3c8c</guid>
      <description>&lt;p&gt;&lt;em&gt;Agent memory that can unlearn, built on FalkorDB for Graph Hacks. Code: &lt;a href="https://github.com/mtrx28/retcon" rel="noopener noreferrer"&gt;github.com/mtrx28/retcon&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiw26nlhgihw8mf2d0qqq.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%2Fiw26nlhgihw8mf2d0qqq.png" alt="After the retraction: what rested only on the portal is red; the contract-backed address stays green" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; When a source your agents trusted turns out to be poisoned, deleting it does almost nothing. We store the &lt;em&gt;reasons&lt;/em&gt; behind every memory in FalkorDB, so one query retracts everything that depended on that source and keeps whatever a second source still supports.&lt;/p&gt;




&lt;p&gt;On October 7th our accounts-payable agent paid a $6,000 invoice to the bank account listed on the vendor's web portal. The portal had been hacked on the 2nd. We found out on the 9th.&lt;/p&gt;

&lt;p&gt;So what do you do on the 10th?&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting the source doesn't work
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to delete the portal page from the agent's memory. It barely helps. By the 7th, one agent had copied the new bank account into its own notes, another had marked it as confirmed, and the Payments agent had turned it into a plan and then a payment. Delete the page and all of that is still there.&lt;/p&gt;

&lt;p&gt;The real question is: &lt;em&gt;what did my agents believe, plan and do because of this source?&lt;/em&gt; A vector store can't answer that. It stores text and embeddings, not reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the reasons
&lt;/h2&gt;

&lt;p&gt;So RETCON stores every memory in FalkorDB together with the reasons it exists: which source said it, and which memories it was built from. One rule decides what the agents believe:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A memory is believed if at least one of its reasons is fully intact.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's why that matters. The portal told us two things about the vendor, Coastal Produce: a new bank account, and its remit address. The address also appears in a signed contract. The bank account appears nowhere else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;compromised portal ──► "bank = SYN-9000-5305" ──► note ──► plan ──► $6,000 payment   ✗ OUT
        │
        └────────────► "remit address" ◄──── signed contract                         ✓ stays IN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Distrust the portal and the address still has a reason, so it stays. The bank account loses its only reason, and takes the note, the plan and the payment with it. Nothing is deleted: memories are marked OUT, and one click puts them back.&lt;/p&gt;

&lt;p&gt;In the graph, a retraction marks everything downstream of the source OUT, then runs this query until it returns nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;j:&lt;/span&gt;&lt;span class="n"&gt;Justification&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:JUSTIFIES&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b:&lt;/span&gt;&lt;span class="n"&gt;Mem&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;status:&lt;/span&gt;&lt;span class="s1"&gt;'OUT'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;run:&lt;/span&gt;&lt;span class="n"&gt;$run&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;OPTIONAL&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Mem&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:PREMISE_OF&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
     &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;p.status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'OUT'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dead&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;total&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="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;dead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;b.status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'IN'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b.run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b.hop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;b.id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A memory comes back only if one of its justifications has every premise alive. Because everything downstream starts OUT, two memories that only vouch for each other can't bring each other back.&lt;/p&gt;

&lt;p&gt;The idea is old (truth maintenance systems, late 1970s). A graph database turns it into a query that works across every agent and every session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview, commit, recall
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Preview&lt;/strong&gt; runs the whole retraction on a copy of the graph, made with FalkorDB's &lt;code&gt;GRAPH.COPY&lt;/code&gt;, and then throws the copy away. In our demo it reads: one source, two observations, four beliefs, two plans, three actions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2y911ei1kczbsioo1ak0.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%2F2y911ei1kczbsioo1ak0.png" alt="Preview on a GRAPH.COPY fork" width="799" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit&lt;/strong&gt; does it for real. A pending $34,500 payment is blocked. The $6,000 that already left lands on a recall list with a drafted bank recall, and nothing is sent until a person approves it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwdrjszed79x6xashjdzs.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%2Fwdrjszed79x6xashjdzs.png" alt="Recall list with drafts awaiting approval" width="718" height="806"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the title. Retraction can fix what your agents believe. It can't un-send a wire.&lt;/p&gt;

&lt;h2&gt;
  
  
  How fragile is a payment?
&lt;/h2&gt;

&lt;p&gt;The same graph answers a question &lt;em&gt;before&lt;/em&gt; an attack: how many independent sources would someone have to compromise to fake this payment? In our demo, the $34,500 payment scored 1. One web page away from fraud. So a gate refuses any payment over $10,000 that rests on fewer than two sources.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fncinfjitbtdrdefjyfir.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%2Fncinfjitbtdrdefjyfir.png" alt="Fragility radar: every decision rests on one source" width="718" height="1050"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We compute that number exactly. FalkorDB's built-in &lt;code&gt;algo.maxFlow&lt;/code&gt; gives a quick second opinion, but it treats "needs A and B" as "A or B", so it can only be an upper bound: it says 4 for that same payment. We show both and trust the exact one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark that said we'd built nothing
&lt;/h2&gt;

&lt;p&gt;We tested five ways to retract: delete the source, delete similar memories, invalidate one hop, taint everything downstream, and ours.&lt;/p&gt;

&lt;p&gt;The first three left 87 to 94 percent of the contamination in place on our plain scenario. Easy wins. But "taint everything downstream" tied us: nothing left behind, nothing lost. For a while our project looked like a very long way to write a recursive delete.&lt;/p&gt;

&lt;p&gt;The two rules only differ when an honest memory sits downstream of a bad source but has a second reason to exist. Our test data had almost none. So we built a scenario that did: five hacked vendors, with about 40 percent of what they touched also confirmed somewhere else.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Contamination left&lt;/th&gt;
&lt;th&gt;Correct payments wrongly flagged&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delete source / similar / one hop&lt;/td&gt;
&lt;td&gt;75-87%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taint everything downstream&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;36 ($1,163,150; 18 already paid)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RETCON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Taint-everything flagged 36 correct payments. That's 36 lines somebody reconciles by hand on Monday.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffy97hbzne5r6mhrfbvnb.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%2Ffy97hbzne5r6mhrfbvnb.png" alt="The receipt: $1.16M of correct payments flagged" width="799" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To be fair, that scenario is constructed. It shows the capability matters, not how often it does. On real data, 40 retractions from the Retraction Watch database replayed on their real dates, taint-everything loses only 1 percent of good memories. But that 1 percent is exactly the 24 claims that several independent papers agreed on: the ones a research agent would rely on most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we didn't trust our own zero
&lt;/h2&gt;

&lt;p&gt;RETCON scored zero on both counts, and that made us suspicious. We had written both the cascade and the answer key.&lt;/p&gt;

&lt;p&gt;So we wrote a second, deliberately naive version of the same rule, about sixty lines, and made RETCON agree with it on a thousand random graphs. Almost immediately it found two bugs in our reinstate logic that every unit test had missed. That checker, not the benchmark, is why we believe the zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take the graph away
&lt;/h2&gt;

&lt;p&gt;Same agents, same attack, but similarity search instead of reasons. It retracted three of nine bad memories and lost five good ones. With the graph: ten of ten, none lost.&lt;/p&gt;

&lt;p&gt;We wanted the headline "without the graph, agents pay the attacker". They did, in our offline demo with a simple hashing embedder. With a real embedding model, they didn't. So we dropped that headline. The honest version: memory without reasons forgets too little, and forgets the wrong things.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bad science data.&lt;/strong&gt; 18 percent of our first "claims" weren't claims at all: author lists, "this review describes".
After fixing the filter, a fresh sample still had 4 bad out of 50. The reader of that sample was a model, and the repository says so; a human pass is still pending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A wobbly model.&lt;/strong&gt; With a looser prompt, the Gemini-powered Payments agent once held a payment it should have made.
That's fine by design: the model picks actions, but the graph and the gate decide what's true and what's allowed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A known FalkorDB edge case.&lt;/strong&gt; &lt;code&gt;algo.pageRank&lt;/code&gt; and &lt;code&gt;algo.WCC&lt;/code&gt; fail on exactly one node, already reported as issue
&lt;a href="https://github.com/FalkorDB/FalkorDB/issues/3349" rel="noopener noreferrer"&gt;#3349&lt;/a&gt;. It would have broken a brand-new tenant's first page, so we added a guard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nulls.&lt;/strong&gt; A payment with no amount sorted to the top of the recall list, because that's what nulls do under &lt;code&gt;DESC&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And RETCON is slower than the naive rules: 47 ms for a retraction among nearly ten thousand memories, where they take a few. With fifty agents writing during a retraction, 1,620 operations left no stale answer. Fast enough, and the extra time buys the second-source check.&lt;/p&gt;

&lt;p&gt;One thing we don't claim: "contaminated" means "rests only on a source you stopped trusting". It doesn't mean "false".&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mtrx28/retcon.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;retcon
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;127.0.0.1:8200&lt;/code&gt; and press &lt;strong&gt;Retract&lt;/strong&gt; at the bottom of the page. No API key needed. RETCON is also an MCP server, so Claude Code can ask it which source to audit first.&lt;/p&gt;

&lt;p&gt;Agents are getting good at learning. The harder part is helping them stop believing something, and showing you what that belief already did.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hackathon</category>
      <category>wemakedevs</category>
      <category>falkordb</category>
    </item>
  </channel>
</rss>
