<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Rui Lu]]></title><description><![CDATA[Rui Lu]]></description><link>https://yanqilu.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 03 Nov 2026 03:38:42 GMT</lastBuildDate><atom:link href="https://yanqilu.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[My coding agent almost undid a decision it never knew existed]]></title><description><![CDATA[Built for WeMakeDevs × FalkorDB Graph Hacks: Context for AI Agents. Primary track: Agents That Act on Connected Data (also Company Brain and Agent Memory)…]]></description><link>https://yanqilu.hashnode.dev/my-coding-agent-almost-undid-a-decision-it-never-knew-existed</link><guid isPermaLink="true">https://yanqilu.hashnode.dev/my-coding-agent-almost-undid-a-decision-it-never-knew-existed</guid><category><![CDATA[agents]]></category><category><![CDATA[falkordb]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Rui Lu]]></dc:creator><pubDate>Sun, 18 Oct 2026 16:35:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad4f373904808461f2d7879/4b21d395-3d0d-4ea8-916e-2745c024638f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/lurui1997/rationale-guard/main/docs/img/cover.png" alt="Rationale Guard cover" /></p>
<p><em>Built for WeMakeDevs × FalkorDB <strong>Graph Hacks: Context for AI Agents</strong>. Primary track: Agents That Act on Connected Data (also Company Brain and Agent Memory).</em>
<em>Live app: <a href="https://rationale-guard-production.up.railway.app">https://rationale-guard-production.up.railway.app</a> · Demo video: <a href="https://github.com/lurui1997/rationale-guard/releases/download/demo-video-v1/rationale-guard-demo.mp4">https://github.com/lurui1997/rationale-guard/releases/download/demo-video-v1/rationale-guard-demo.mp4</a> · Code (MIT): <a href="https://github.com/lurui1997/rationale-guard">https://github.com/lurui1997/rationale-guard</a></em></p>
<h2>The one-line diff</h2>
<p>The pull request looked harmless. In <code>packages/adapter-utils/src/billing.ts</code> someone, or something, changed one expression:</p>
<pre><code class="language-diff">-  const price = usage.cost ?? null;
+  const price = usage.cost ?? 0; // treat unknown as zero
</code></pre>
<p>A reviewer skimming the diff sees a null turned into a real value. A coding agent sees a type error go away. Neither of them is looking at issue <a href="https://github.com/paperclipai/paperclip/issues/14548">#14548</a> on <a href="https://github.com/paperclipai/paperclip">paperclipai/paperclip</a>. That issue is still open, and it says the cost API returns <code>costCents: 0</code> with no way to tell "the cost is zero" from "we don't know the cost." They are also not looking at PR <a href="https://github.com/paperclipai/paperclip/pull/14991">#14991</a>, which says, verbatim:</p>
<blockquote>
<p>Classify managed custom Responses routes as API usage with their routed biller; retain unknown prices instead of treating them as ChatGPT subscriptions or direct OpenAI.</p>
</blockquote>
<p>and, in a sibling decision from the same area:</p>
<blockquote>
<p>Preserve explicit zero prices and missing prices as distinct states, including later unpriced Gemini usage.</p>
</blockquote>
<p>Those sentences are the decision, and the one-line diff contradicts them. The premise is still live, because #14548 was <em>referenced</em> and never closed. The person to ask is the human who actually edits those files.</p>
<p>Agents, and developers in their first week, undo decisions they never saw. Rationale Guard tells you before the change merges.</p>
<p><img src="https://raw.githubusercontent.com/lurui1997/rationale-guard/main/docs/img/guard-warn.png" alt="Guard result on the cost diff: contradiction, verbatim quote, premise, who to ask" /></p>
<h2>Why a graph and not RAG</h2>
<p>A diff has paths and lines. It doesn't contain a question. Embedding "unknown cost" and fetching the nearest paragraphs will turn up #14548, along with a dozen unrelated pricing comments. It won't tell you that #14991 links the issue with <code>Refs</code>, not <code>Fixes</code>, that the issue is <code>OPEN</code> today, or that someone overrode the decision last week. That's all state on edges, and similarity search doesn't see edges.</p>
<p>The question a diff actually asks is about structure:</p>
<blockquote>
<p>Which past decision was implemented by a change to <em>this file</em>? Does that decision still rest on an open issue, a pending ADR, an unmerged dependency, a reverted commit? What did the authors refuse to do instead? Who still works here?</p>
</blockquote>
<p>In Rationale Guard that question is a set of parameterized multi-hop Cypher queries. This is the first one, taken directly from <code>rationale/guard.py</code>:</p>
<pre><code class="language-cypher">MATCH (f:File) WHERE f.path IN $paths
MATCH (pr:PR)-[:TOUCHES]-&gt;(f)
MATCH (d:Decision)-[:DERIVED_FROM]-&gt;(pr)
WHERE coalesce(d.status, 'accepted') &lt;&gt; 'superseded'
  AND coalesce(d.extractor, '') &lt;&gt; 'correction'
RETURN d.id AS decision_id, d.source_quote AS quote, d.source_url AS source_url,
       f.path AS path, pr.number AS via_number, 'pr-file' AS hop
</code></pre>
<p>Sibling queries go one hop further. They follow <code>File ← TOUCHES ← PR → FIXES/REFERENCES → Issue ← DERIVED_FROM ← Decision</code> and <code>PR → ADDS → Doc ← DERIVED_FROM ← Decision</code>, so a decision written down in an ADR still catches a diff to a file that came in with the PR that added the ADR. A symbol hop covers renamed or new paths that use old identifiers.</p>
<p>Premise checks are Cypher too. Here is rule R1 from <code>rationale/premises.py</code>, the one that fired on the cost diff:</p>
<pre><code class="language-cypher">MATCH (d:Decision)-[:DERIVED_FROM]-&gt;(pr:PR)-[r:REFERENCES]-&gt;(i:Issue {state: 'OPEN'})
WHERE d.id IN $ids
RETURN d.id AS decision_id, pr.id AS left_id, i.id AS right_id,
       'linked issue #' + toString(i.number) + ' is still open' AS detail
</code></pre>
<p>The difference between the two variants matters. If a merged PR says <code>Fixes #N</code> and the issue is still open, the "we fixed it" premise is stale, and that's a warning. If it only says <code>Refs #N</code>, the reason for the decision is still open, so the premise holds. Other rules read ADR status (R3), follow <code>SUPERSEDES</code> (R4), follow <code>REVERTS</code> (R7), and mark an author inactive only when GitHub's public events say so (R6).</p>
<p>The UI shows every query it ran. Open <strong>Cypher</strong> on a finding and you get the exact statements and their timings. The reasoning-path chips are the nodes those queries returned.</p>
<p><img src="https://raw.githubusercontent.com/lurui1997/rationale-guard/main/docs/img/guard-cypher.png" alt="Executed Cypher and reasoning path" /></p>
<h2>What FalkorDB does here</h2>
<p>The graph isn't a place to park JSON. One FalkorDB instance gives us:</p>
<ul>
<li><strong>One graph per repository.</strong> The seed is <code>rationale__paperclipai__paperclip</code>. Paste a public PR URL and the app builds <code>rationale__Kludex__starlette</code> next to it, so the two companies' memories never mix.</li>
<li><strong>Fast parameterized multi-hop Cypher.</strong> The candidate, premise, alternative, correction and people queries run as five concurrent calls. The graph step is about 220 ms warm on the hosted Railway deployment.</li>
<li><strong>Graph algorithms as procedures.</strong> We materialize <code>COLLABORATED</code> edges between people who edit the same files, then rank them in the database:</li>
</ul>
<pre><code class="language-cypher">CALL algo.pageRank('Person', 'COLLABORATED') YIELD node, score
RETURN node.login AS login, score
</code></pre>
<p><code>algo.betweenness</code> is called the same way. The ranked list is intersected with the people who actually edited the files in the diff. On the cost files that's <code>@devinfoley</code>, and the card reports a bus factor of 1, because it is 1.</p>
<ul>
<li><strong>Indexes plus fulltext.</strong> There are range indexes on every node id, and <code>db.idx.fulltext.createNodeIndex('Decision', 'summary', 'rationale', 'source_quote')</code> backs the secondary "why" lookups.</li>
</ul>
<p><img src="https://raw.githubusercontent.com/lurui1997/rationale-guard/main/docs/img/architecture.png" alt="Architecture" /></p>
<p>Ingest goes from the public GitHub API, through rule-based edges (<code>Fixes</code> gives <code>FIXES</code>, <code>Refs</code> gives <code>REFERENCES</code>, and <code>[bot]</code> accounts are kept for audit but left out of "who to ask"), then a DeepSeek extractor (or an explicit offline extractor). A decision is kept <strong>only if its quote is a substring of the source artifact</strong>.</p>
<h2>The guard flow</h2>
<ol>
<li><strong>Paste a diff or a public PR URL.</strong> For a URL, the app fetches up to 20 changed files and walks their history from the last 18 months, capped at 30 PRs. It extracts decisions, merges them into that repo's graph, and guards the PR's own diff.</li>
<li><strong>The graph selects candidates.</strong> These are the Cypher hops above, followed by the premise rules, rejected <code>Alternative</code> nodes (<code>REJECTED_FOR</code>), and experts.</li>
<li><strong>A judge labels them.</strong> The DeepSeek judge gets the verbatim quote and the hunk, and has to return <code>contradicts</code>, <code>related</code>, or <code>unrelated</code>. Unrelated candidates are dropped. The model never retrieves anything. It only labels what the graph returned. If you asked for the LLM judge and the call fails, the request fails. There's no silent fallback.</li>
<li><strong>The card shows its evidence:</strong> the verbatim quote and source link, premise status and rule IDs, rejected alternatives, who to ask, the reasoning-path subgraph, and the executed Cypher.</li>
</ol>
<p>A README typo touches no file that carries a decision, so it passes:</p>
<p><img src="https://raw.githubusercontent.com/lurui1997/rationale-guard/main/docs/img/guard-pass.png" alt="Benign diff passes" /></p>
<p>The same thing works on a repository we didn't seed. I pasted <a href="https://github.com/Kludex/starlette/pull/3651">Kludex/starlette#3651</a>, and the app built a Starlette graph from public data. Then I pasted a diff that turns <code>if self.max_age is not None:</code> back into <code>if self.max_age:</code>. Guard warned on the decision <em>"Treat <code>max_age=0</code> as an explicit value rather than as unset in <code>SessionMiddleware</code>"</em> and quoted the PR:</p>
<blockquote>
<p><code>SessionMiddleware</code> checked <code>if self.max_age</code>, so <code>max_age=0</code> was treated like <code>None</code> and the cookie became a browser-session cookie. It now checks <code>is not None</code>, matching how <code>max_age</code> is passed to <code>unsign</code>.</p>
</blockquote>
<p>It recommended asking <code>@Kludex</code> first, by PageRank.</p>
<p><img src="https://raw.githubusercontent.com/lurui1997/rationale-guard/main/docs/img/guard-starlette-warn.png" alt="Starlette max_age reversal warns" /></p>
<h2>Memory from corrections</h2>
<p>"The agent remembers" shouldn't mean a 24-hour cache. In Rationale Guard, every correction is a node:</p>
<pre><code class="language-cypher">MERGE (c:Correction {id: $id})
SET c.kind = $kind, c.text = $text, c.by = $by, c.at = $at
MERGE (c)-[:ABOUT]-&gt;(d:Decision {id: $decision})
</code></pre>
<ul>
<li><strong>ack</strong> keeps the finding and records who acknowledged it and why. The next run shows "Learned from @… : ack".</li>
<li><strong>wrong</strong> multiplies that decision's match score by 0.25, so it drops below better candidates.</li>
<li><strong>override</strong> marks the old decision <code>superseded</code> and writes a new <code>Decision</code> that <code>SUPERSEDES</code> it, using the reason you typed. The next guard run doesn't warn on the old quote, and it says it learned the override.</li>
</ul>
<p>Because corrections live in FalkorDB, they survive restarts and re-ingests. Re-ingesting a repo merges new history and never deletes <code>Correction</code> nodes.</p>
<p><img src="https://raw.githubusercontent.com/lurui1997/rationale-guard/main/docs/img/guard-learned.png" alt="A correction is remembered and shown on the rerun" /></p>
<h2>MCP and the GitHub Action: the agent acts</h2>
<p>The point is for the <em>agent</em> to ask the graph before it edits. <code>python3 -m rationale.mcp_server</code> (stdio) and <code>POST /mcp</code> (JSON-RPC over HTTP) expose five tools: <code>guard_diff</code>, <code>why</code>, <code>who_knows</code>, <code>check_premises</code>, <code>record_feedback</code>. Here is a real call to the live server:</p>
<p><img src="https://raw.githubusercontent.com/lurui1997/rationale-guard/main/docs/img/mcp-guard-diff.png" alt="MCP guard_diff over HTTP" /></p>
<p>It returns the same decision, the same premise (<code>linked issue #14548 is still open</code>), and the same expert, in a form an agent can act on, for example by keeping unknown as <code>null</code>.</p>
<p>For humans, <code>action/post_guard.py</code> runs as a GitHub Action. It upserts one sticky comment marked <code>&lt;!-- rationale-guard --&gt;</code> and posts a neutral check run on the PR head SHA. <code>/rationale ack|override|wrong &lt;reason&gt;</code> is accepted only from commenters with write access.</p>
<h2>Honest numbers</h2>
<p>These are from the live deployment (Railway, FalkorDB, DeepSeek judge) on the 16 labelled diffs in <code>eval/cases.yaml</code>:</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>Recall on contradicting diffs</td>
<td><strong>0.89</strong> (8/9)</td>
</tr>
<tr>
<td>False positives on benign diffs</td>
<td><strong>0</strong> (0/7)</td>
</tr>
<tr>
<td>Cue-word contradictions</td>
<td>6/6</td>
</tr>
<tr>
<td>Paraphrased contradictions</td>
<td>2/3</td>
</tr>
<tr>
<td>Guard end to end</td>
<td>~1.7 s</td>
</tr>
<tr>
<td>Graph step (warm)</td>
<td>~220 ms</td>
</tr>
</tbody></table>
<h2>Limitations</h2>
<ul>
<li><strong>Small eval.</strong> Sixteen cases is a sanity check, not a benchmark. Paraphrased contradictions are the weak spot (2/3).</li>
<li><strong>Seed scope.</strong> The seed covers three areas of one public repo (cost accounting, idle sleep, MCP access). It isn't a whole company. Anything you haven't ingested doesn't exist to the guard.</li>
<li><strong>Extraction misses things.</strong> The committed seed came from the deterministic offline extractor. It misses decisions phrased without decision-like cue words. The LLM extractor is stricter, and both drop any quote that isn't verbatim.</li>
<li><strong>Thin PageRank.</strong> On a graph built from a handful of PRs PageRank is real but thin, and many files have a bus factor of 1.</li>
<li><strong>It doesn't block merges.</strong> The Action's check is neutral on purpose. It comments, and you can make the check required yourself.</li>
<li><strong>GitHub rate limits.</strong> Ingest needs <code>GITHUB_TOKEN</code>. One PR costs roughly 25–30 GitHub API requests, and ingest on the public demo is rate-limited.</li>
<li><strong>Open feedback endpoint.</strong> <code>POST /api/feedback</code> on the HTTP API is unauthenticated. The Action checks write access before calling it, so keep the API behind a network boundary in real use.</li>
</ul>
<h2>What's next</h2>
<ul>
<li>A larger, harder eval set focused on paraphrased and multi-file contradictions.</li>
<li>Incremental ingest from webhooks instead of on-demand history walks.</li>
<li>Pulling decisions from more sources (ADRs across repos, design docs, review threads) into the same per-repo graph.</li>
<li>A pre-commit hook and editor integration that call <code>guard_diff</code> locally.</li>
<li>Ways to weight time and authorship (a decision by someone still active vs. one whose authors have all left).</li>
</ul>
<h2>Links</h2>
<ul>
<li>Live app: <a href="https://rationale-guard-production.up.railway.app">https://rationale-guard-production.up.railway.app</a></li>
<li>Code (MIT): <a href="https://github.com/lurui1997/rationale-guard">https://github.com/lurui1997/rationale-guard</a></li>
<li>Demo video: <a href="https://github.com/lurui1997/rationale-guard/releases/download/demo-video-v1/rationale-guard-demo.mp4">https://github.com/lurui1997/rationale-guard/releases/download/demo-video-v1/rationale-guard-demo.mp4</a></li>
<li>FalkorDB: <a href="https://www.falkordb.com">https://www.falkordb.com</a></li>
<li>Hackathon: WeMakeDevs × FalkorDB, Graph Hacks: Context for AI Agents</li>
</ul>
<p><em>The agent wasn't wrong about JavaScript. It was wrong about a decision it never saw. The graph is how you hand that decision to the agent without trusting the agent to remember it.</em></p>
]]></content:encoded></item></channel></rss>