<?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: Swapnanil Saha</title>
    <description>The latest articles on DEV Community by Swapnanil Saha (@swapnanilsaha).</description>
    <link>https://dev.to/swapnanilsaha</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%2F3939906%2F9f37b94e-be6e-42b9-a63e-34b65dca3522.jpeg</url>
      <title>DEV Community: Swapnanil Saha</title>
      <link>https://dev.to/swapnanilsaha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swapnanilsaha"/>
    <language>en</language>
    <item>
      <title>When a Memory Should Become a Check: Advice, Enforcement, and Agent Memory</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Tue, 27 Oct 2026 16:18:55 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/when-a-memory-should-become-a-check-advice-enforcement-and-agent-memory-3p56</link>
      <guid>https://dev.to/swapnanilsaha/when-a-memory-should-become-a-check-advice-enforcement-and-agent-memory-3p56</guid>
      <description>&lt;p&gt;&lt;em&gt;Delivered as text, a memory is advice, and advice works only if the agent believes it. Compiled into a check, it needs no belief, but a rule that is always enforced is never tested again. The design question is which memories should become checks, and what a check has to keep so that it can still be retired.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A memory delivered as text is advice. It works only if the agent believes it, and agents are sceptical of text they cannot check. In a set of controlled runs, an agent that received a correct note, in full and on the turn where it mattered, still declined to act on it, and said why: the claim had not come from the user, and nothing in the repository backed it up.&lt;/p&gt;

&lt;p&gt;A memory compiled into a check does not need belief. The check runs, and the action passes or it does not. Recent work that turns a user's corrections into runtime checks cut repeat violations well below what the same corrections achieved as stored memory.&lt;/p&gt;

&lt;p&gt;That seems to settle the matter: compile every memory that can be compiled. It does not, because enforcement carries a cost that advice does not. &lt;strong&gt;A rule that is always enforced can never be tested again.&lt;/strong&gt; If the agent can never take the forbidden action, nobody observes what the action would do now, and a rule that has outlived its reason stays in force with nothing to retire it.&lt;/p&gt;

&lt;p&gt;The question that remains is narrower than advice versus enforcement: &lt;strong&gt;which memories should become checks, and what a check has to carry so that it can still be retired.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The Belief Problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  01 · Advice Has a Belief Problem: Why Agents Decline Correct Memory Notes
&lt;/h3&gt;

&lt;p&gt;The runs that exposed the problem were built to separate belief from the other ways a memory can fail. They come from an evaluation harness in vectr, an open-source code-search and agent-memory tool I maintain. Each scenario is a small synthetic repository with one trap and one planted note that describes the trap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A generated config.&lt;/strong&gt; &lt;code&gt;config/limits.conf&lt;/code&gt; is produced by &lt;code&gt;tools/gen_limits.py&lt;/code&gt;, so a hand edit is silently reverted the next time the file is regenerated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A superseded API.&lt;/strong&gt; The file being edited still calls &lt;code&gt;format_currency_legacy()&lt;/code&gt;; the correct call is &lt;code&gt;format_currency()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A flaky test.&lt;/strong&gt; &lt;code&gt;./run_tests.sh&lt;/code&gt; includes a test that fails at random, and &lt;code&gt;./run_tests.sh --core&lt;/code&gt; is the deterministic subset to run instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scoring is mechanical. The scorer looks at the final bytes of files and at the commands the agent actually executed, never at its prose, so an agent that says it will use the new function and then does not scores the same as one that says nothing. Every scenario also runs with the note withheld, as a control, and in the first full round those controls walked into the trap in all seven trap scenarios tried. In these runs, "believing" a note means only that it changed which files the agent wrote or which commands it ran.&lt;/p&gt;

&lt;p&gt;The first rounds failed for mechanical reasons. At first the note was retrieved and never delivered, because the one request it was spent on could not carry it. Then it was delivered as a title only, on a single early turn, and in six of seven baseline runs it had left the agent's context before the first edit. Those were delivery bugs in the harness, and they were fixed: the full note, delivered on the turn where the decision is made.&lt;/p&gt;

&lt;p&gt;With delivery fixed, the note changed what the agent did in two of the three scenarios. In the third, the flaky test, it did not, and it did not through a second, independent delivery channel either: a hook that hands the note to the agent as it is about to use a tool, instead of text appended to its request. An earlier round had already produced the sharpest version of the same result. With the note on the decision turn, in full, and stripped of the cautionary framing it was normally wrapped in, the agent declined to act on it and gave its reasons: the claim had not come from the user, and nothing in the repository corroborated it. In that round, changing the wrapper, sending the full body, and presenting the note as the user's own each left the outcome where it was, at two runs per variant.&lt;/p&gt;

&lt;p&gt;What separated the two successes from the failure was not the channel or the wording. &lt;strong&gt;In both successes, the agent could confirm the note by reading a second file&lt;/strong&gt;: the generator script in one case, the replacement function's definition in the other. The transcript of one configuration run shows the agent opening &lt;code&gt;tools/gen_limits.py&lt;/code&gt; and establishing from it that the config file is generated. In the flaky-test scenario the note was the only source for its claim. Nothing in that repository recorded that a test was flaky, and seeing it takes repeated runs of the suite. A real project might have a quarantine list or a flaky marker on the test; the scenario left both out on purpose, so that the note stood alone. On the second channel the agent applied the note's other piece of advice, a rounding fix in the code, while still declining the test command it had no way to check.&lt;/p&gt;

&lt;p&gt;Each of these results is one run per scenario and channel. They show a pattern, not a rate, and the harness's own write-up says so.&lt;/p&gt;

&lt;p&gt;The general reading: a delivered note competes with the agent's own judgment, and the judgment wins when the note is uncorroborated and asks for something the agent would not otherwise do. Running the whole test suite is what a careful engineer does. A note from nowhere asking to skip part of it is the kind of text a careful agent ought to distrust.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Two sticky notes on a new desk&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A contractor starts on Monday and finds two sticky notes on the desk. One says the rate-limit config is generated, so edit the generator and not the file. There is a script called &lt;code&gt;gen_limits.py&lt;/code&gt; in the tools folder, and thirty seconds of reading confirms it. The other says one of the tests is flaky and to run only the core suite. Nothing on disk says so, and the note is unsigned. A good contractor follows the first note and runs the full suite anyway, because an anonymous request to skip tests is what a careful person is supposed to refuse.&lt;/p&gt;

&lt;p&gt;The analogy breaks in one place. The contractor can walk over and ask someone. An agent running unattended cannot, so a note it cannot check is simply outvoted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The distrust is working as intended&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An agent that does whatever unverified text in its context tells it to is the security problem, not the fix: instructions planted in auto-loaded memory files get obeyed often enough to be a named attack surface. Making delivered notes more persuasive across the board would reopen that hole, and Deterministic, Reproducible, and Wrong, my technical report on corroborating memory notes, calls it a security regression outright. Whatever fixes the belief problem has to work without asking the agent to believe more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/blog/agents-over-trust-instructions-and-under-trust-corrections/" rel="noopener noreferrer"&gt;Agents Over-Trust Instructions and Under-Trust Corrections&lt;/a&gt; works through why planted instructions get obeyed while true corrections get ignored, and why the deciding factor is the shape of the text rather than its source. For deciding where enforcement belongs, one consequence is enough: louder wording does not close the gap, because what the agent lacks is evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: The Alternative
&lt;/h2&gt;

&lt;h3&gt;
  
  
  02 · A Check Does Not Need to Be Believed: Compiling User Corrections Into Runtime Enforcement
&lt;/h3&gt;

&lt;p&gt;The strongest recent evidence for the alternative is TRACE, for Test-time Rule Acquisition and Compiled Enforcement (Zhou et al., 2026). It watches a user's conversation with a coding agent for corrections, such as "You left debug files again. Clean them before stopping." Each correction is rewritten as an atomic rule with an applicability condition that says when it should and should not fire, and compiled into three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;strong&gt;applicability check&lt;/strong&gt;, which decides whether the rule is in play for a given task;&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;behaviour instruction&lt;/strong&gt;, text placed in the agent's context while the rule is active;&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;verifier&lt;/strong&gt;, attached to a hook that fires at a prompt, a tool call, a file write or the end of the task. If the verifier fails, the hook interrupts the event and hands the violation back to the agent, which has to fix it before the task is allowed to finish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the compiled form still carries the advice. What it adds is a gate that does not care whether the advice was believed. The retry loop is bounded, at three retries per task in the paper's diagnostic, after which the run ends with the violation logged.&lt;/p&gt;

&lt;p&gt;The evaluation removes the target preference from every test prompt and freezes each condition's rule or memory store after a shared training stream, so the agent can satisfy a preference only through what it acquired earlier. An agent with no memory therefore violates every one, which is why each result below is a reduction from 100%. On held-out tasks from ClawArena, a coding-agent benchmark, TRACE cut violations to &lt;strong&gt;37.6%&lt;/strong&gt; in distribution and to &lt;strong&gt;2.0%&lt;/strong&gt; on task families it had never seen, with task pass in the same band as having no memory at all. The strongest memory baseline, Mem0, left 51.1% violated in distribution. On memory-intensive tasks derived from MemoryArena, violations fell from 100.0% to 60.5%, while task pass matched or exceeded the strongest memory baseline.&lt;/p&gt;

&lt;p&gt;The cleanest comparison is a diagnostic earlier in the paper, because it holds the knowledge fixed and changes how it reaches the agent. It uses 19 held-out tasks built from real correction cases, with 29 preference checks among them, and averages over six models.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;What the agent gets&lt;/th&gt;
&lt;th&gt;Compliance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No rules&lt;/td&gt;
&lt;td&gt;The task, with the preference removed&lt;/td&gt;
&lt;td&gt;31.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mem0&lt;/td&gt;
&lt;td&gt;Past corrections retrieved from a memory store&lt;/td&gt;
&lt;td&gt;42.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relevant rules&lt;/td&gt;
&lt;td&gt;The rules that apply to this task, as text&lt;/td&gt;
&lt;td&gt;54.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All rules&lt;/td&gt;
&lt;td&gt;The user's whole rule set, as text&lt;/td&gt;
&lt;td&gt;55.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiled rules&lt;/td&gt;
&lt;td&gt;The rules that apply, as checks that gate completion&lt;/td&gt;
&lt;td&gt;70.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Mean compliance across six models on 29 preference checks (Zhou et al., 2026, Section 3.2). Relevant rules and compiled rules draw on the same rule library; what differs is the representation, and the feedback loop that comes with a check.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The like-for-like pair is relevant rules against compiled rules: the same applicable rules reached 54.0% as text and 70.1% as checks that gate completion. Part of that gap is the retry loop, since only the compiled condition gets its failures fed back and up to three more attempts. That is not a confound to subtract. Feedback on a failed check is most of what a check is. Putting the rules in context bought 22 points over nothing; turning them into checks bought 16 more.&lt;/p&gt;

&lt;h4&gt;
  
  
  What the numbers do and do not show
&lt;/h4&gt;

&lt;p&gt;Three things bound them. The users in the main benchmarks are simulated: a model built from a real user's correction history decides when to correct the agent, validated against held-out real correction moments at a decision F1 of 0.906. The diagnostic draws on one researcher's two months of transcripts, deliberately, so that preferences do not conflict across users. And enforcement covers only what has been compiled. Compiled rules still left 29.9% of the diagnostic's checks unmet, and on unseen families of memory tasks the violation rate stayed at 97.0%, which the authors attribute to constraints no compiled rule covered.&lt;/p&gt;

&lt;p&gt;What changed is who has to do the believing. A check turns a claim the agent must accept on trust into an observation the harness makes. The agent does not need to be persuaded that the rule is right, only told that the check failed and why. The flaky-test scenario failed on that point: the harness can see which command ran, and a check on the command would never have required the agent to take anyone's word for anything.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A check tests the action, not the rule&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two different things could be checked, and they are easy to run together. One is whether an &lt;em&gt;action&lt;/em&gt; breaks the rule: did this command, this diff, this final workspace violate it? The other is whether the &lt;em&gt;rule&lt;/em&gt; still matches the world: is the reason it was written still true? "The agent's diff touches &lt;code&gt;config/limits.conf&lt;/code&gt;" decides the first. "&lt;code&gt;config/limits.conf&lt;/code&gt; is still generated by &lt;code&gt;tools/gen_limits.py&lt;/code&gt;" decides the second.&lt;/p&gt;

&lt;p&gt;A compiled check needs the first predicate to work at all. Nothing in the enforcement loop needs the second, so nothing in it evaluates the second.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If enforcement were free, the conclusion would be to compile everything that has a predicate. It has two costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: The Cost
&lt;/h2&gt;

&lt;h3&gt;
  
  
  03 · An Enforced Rule Cannot Be Tested Again
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/blog/a-directive-is-not-a-memory/" rel="noopener noreferrer"&gt;A Directive Is Not a Memory&lt;/a&gt; makes the underlying argument: obeying a directive destroys the evidence that would test it. A rule like "never run the tests with the system interpreter" is followed, and following it produces a record of compliance, not of correctness. The tests pass in the virtual environment, CI uses the virtual environment, the docs say to use it. Every one of those signals is a consequence of the rule having been obeyed, and none of them says whether the rule is still needed.&lt;/p&gt;

&lt;p&gt;Advice leaves a gap in that loop. An agent that ignores advice sometimes generates the counterfactual: it runs the forbidden command, and the result shows whether the rule still holds. The flaky-test agent that ran the full suite anyway was producing that kind of observation, the forbidden action together with its outcome. A blocking check closes the gap. &lt;strong&gt;The more reliably a check is enforced, the less anyone learns about whether it should still exist.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  A rule library has selective labels
&lt;/h4&gt;

&lt;p&gt;Statistics has a name for data whose outcomes exist only where an earlier decision allowed them. In the selective labels problem, the standard example is bail: whether a defendant fails to appear in court is observed only if the judge released them (Lakkaraju et al., 2017). The outcomes that would show whether a detention was necessary are the ones detention prevents. A library of blocking checks is selectively labelled by construction. Every blocked action is an outcome nobody observes, and those are the only outcomes that could say whether the block is still needed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why a moving codebase makes it expensive
&lt;/h4&gt;

&lt;p&gt;Reinforcement learning calls the underlying trade the exploration problem. An agent that always takes the action it currently rates best, the greedy action, stops learning about the others. Sutton and Barto note that when the true values of actions change over time, exploration is needed even if rewards are perfectly predictable, "to make sure one of the nongreedy actions has not changed to become better than the greedy one." A rule library lives in that kind of nonstationary world. The package the system interpreter lacked gets installed globally; the generator script is deleted in a refactor; the deprecated function is finally removed and the lint rule that guarded it now checks for a name nobody can call. A rule's reason is a fact about a codebase that keeps moving.&lt;/p&gt;

&lt;p&gt;The remedy &lt;a href="https://swapnanilsaha.com/blog/a-directive-is-not-a-memory/" rel="noopener noreferrer"&gt;A Directive Is Not a Memory&lt;/a&gt; proposes is a deliberate, scoped test of the forbidden action: take it once, in a sandbox, and record what happened. A check system with no path for that has made its rules permanent without anyone deciding to.&lt;/p&gt;

&lt;h4&gt;
  
  
  Retirement by complaint
&lt;/h4&gt;

&lt;p&gt;TRACE does retire rules. When a new correction contradicts an existing rule, its lifecycle resolver supersedes the old one: a new rule is installed, and the old one is archived and kept on disk for audit and manual rollback. That catches the stale rules that hurt someone enough to prompt a correction. It misses the quiet ones. A user corrects what goes wrong in front of them, and a rule that has become merely unnecessary rarely does anything wrong in front of anyone. It forces a slower path, an extra step, a redundant command, and its cost is paid in small amounts that never show up as an event. A check that blocks an action also hides what the agent would have done, so even a user who is watching sees the constraint and never the case against it.&lt;/p&gt;

&lt;p&gt;Anyone who has inherited a pre-commit configuration knows the result. Every hook in it was added after something went wrong, few of them say what went wrong, and removing one feels like a bet nobody wants to own. Agents are about to write rules like these faster than people ever did.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How fast a rule library goes stale&lt;/strong&gt;&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a = rules added per month
h = rate per month at which a live rule's reason stops holding (h = ln 2 / half-life)
c = share of those lapses an anchor catches (rule flagged, then retired)
r = rate at which an uncaught stale rule is re-tested and retired

dL/dt = a − h·L                 live rules
dS/dt = (1 − c)·h·L − r·S       stale rules still enforced

L* = a / h      S* = (1 − c)·a / r
f* = S* / (L* + S*) = (1 − c)·h / (r + (1 − c)·h)
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;In words: each month a new rules arrive, a fraction h of the live ones lose their reason, anchors catch a share c of those, and the rest stay enforced until something re-tests them. &lt;strong&gt;L&lt;/strong&gt; counts enforced rules whose reason still holds. &lt;strong&gt;S&lt;/strong&gt; counts enforced rules whose reason has lapsed without anything noticing. With a re-test path (r &amp;gt; 0) the library settles, and the stale share f* depends only on how fast reasons lapse, how much of that the anchors catch, and how often the rest is re-tested. Give reasons a half-life of one year (h ≈ 0.058 per month), let anchors catch 60% of lapses, and re-test each uncaught stale rule about once a year (r ≈ 0.083 per month): f* ≈ 0.22.&lt;/p&gt;

&lt;p&gt;With r = 0 there is no steady state. Live rules level off at a/h while uncaught stale rules keep accumulating, so the stale share climbs toward 1 however good the anchors are, unless they catch every lapse. With the same one-year half-life and no anchors, stale rules outnumber live ones after about 27 months; anchors that catch 60% push that to just under five years. Anchors slow the inflow of stale rules, and something else has to drain what they miss. Retirement by complaint is a small r that only reaches the rules that hurt someone; a deliberate re-test path is the general one. Every input here is an assumption rather than a measurement. Reasons do not lapse at a constant rate, anchors also fire on changes that leave the reason intact (each false flag costs an evaluation, not a rule), and a stale rule is not always harmful. The structure is what carries over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive · Demo 1: Watch a Rule Library Age&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set how fast rules are added, how long their reasons last, how many lapses the anchors catch, and how often uncaught rules are re-tested. The chart splits enforced rules over five years into those whose reason still holds and those enforced after their reason is gone. Start with re-tests at zero and watch the stale band grow without limit, then add a small re-test rate and watch it level off. Rules added per month changes the size of the library but not the share. With the defaults (4 rules a month, a one-year half-life, anchors catching 60% of lapses, no re-tests), stale rules outnumber live ones from month 57, and at five years 70 of 135 enforced rules are stale; one re-test per stale rule per year holds the share near 22%. (The interactive version runs on the original page.)&lt;/p&gt;

&lt;p&gt;Expected values of the model in the callout above, stepped monthly, with no randomness and no measured inputs. A re-test is assumed to be conclusive and to retire the rule when its reason is gone; real re-tests are noisier, and each one costs a run of the forbidden action somewhere safe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  04 · Compiled Checks Are Wrong in Their Own Ways
&lt;/h3&gt;

&lt;p&gt;The second cost is that a check can be wrong, and it is wrong with more authority than a note.&lt;/p&gt;

&lt;h4&gt;
  
  
  A mistaken memory becomes a mistaken block
&lt;/h4&gt;

&lt;p&gt;A check compiled from a mistaken memory blocks correct work, and the agent has no standing to argue. A note can be weighed against other evidence. A failed verifier ends the event, and the rule's only voice at that point is its failure message.&lt;/p&gt;

&lt;h4&gt;
  
  
  Satisfied in letter, violated in spirit
&lt;/h4&gt;

&lt;p&gt;A check that is too literal gets satisfied in letter and violated in spirit. ImpossibleBench (Zhong, Raghunathan and Carlini, 2025) measures what coding agents do when a task's unit tests contradict its specification, so that any pass requires a shortcut. With the tests writable and the instructions saying not to modify them, frontier models passed impossible tasks four ways: by editing the tests anyway, by overloading comparison operators so that equality checks always succeed, by recording state so that the same input returns different answers on different calls, and by special-casing the exact inputs the tests use. Making the tests read-only stopped the edits and left other routes in use, special-casing and operator overloading among them.&lt;/p&gt;

&lt;p&gt;The tasks were impossible on purpose, so the rates describe behaviour under pressure rather than on an ordinary day. The structure carries over to compiled rules. A check guards one route to an outcome, and an agent pushed hard toward the outcome can take a route the check does not watch.&lt;/p&gt;

&lt;h4&gt;
  
  
  A check is a program, and programs have bugs
&lt;/h4&gt;

&lt;p&gt;My own evaluation scorer, a deterministic program with no language model in it, matched shell text anywhere in a command. It counted &lt;code&gt;cat deploy.sh&lt;/code&gt; as running the deploy script, and so reported an agent committing the exact mistake a delivered note warned against. The transcript shows the agent reading the script, declining to run it, and citing the note. &lt;a href="https://swapnanilsaha.com/research/under-trusted-memory/" rel="noopener noreferrer"&gt;Deterministic, Reproducible, and Wrong&lt;/a&gt; documents the defect and the guard that would have caught it.&lt;/p&gt;

&lt;p&gt;The same class of error appears in the worked example in TRACE's appendix. Rule F2 comes from a user's request to clean up log files before a project closes. It compiles to a hook on every shell command, and its applicability check is four regular expressions meant to detect a command that writes to a file with a log-style name. As printed, they also match &lt;code&gt;cat run_log_20260915.log&lt;/code&gt; and &lt;code&gt;python analyze.py run_log_20260915.log&lt;/code&gt;, which read a log rather than write one, because the patterns accept the file name anywhere after &lt;code&gt;cat&lt;/code&gt; or &lt;code&gt;python&lt;/code&gt;. The appendix does not report how often patterns like these misfire in practice, and a library of 47 rules is small enough to audit by hand. None of this makes the paper careless. Matching a command's text is the obvious first way to build a command check, and text alone cannot tell naming a file from acting on it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Runs deploy.sh?&lt;/th&gt;
&lt;th&gt;Pattern &lt;code&gt;(\./)?deploy\.sh\b&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;./deploy.sh staging&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;fires&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cd ops &amp;amp;&amp;amp; ./deploy.sh staging&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;fires&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeout 60 bash deploy.sh staging&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;fires&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cat deploy.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;fires&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;grep -n reconcile deploy.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;fires&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;echo 'do not run deploy.sh directly'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;fires&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The bare pattern that broke the scorer fires on every row: right on the first three, wrong on the last three. The corrected matcher anchors to execution position, the start of a command or just after a separator or pipe, through wrappers such as env and timeout.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Test a command check with reads, not only runs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A check on shell commands needs negative cases in its test suite: reads, searches, listings and quoted mentions of the same file, all of which must not fire. Where the outcome is visible in the workspace, compare the two signals as well. If the check says the deploy ran and the deploy state is untouched, one of them is wrong, and the record should fail loudly instead of quietly picking one. That comparison is the contradiction guard the technical report adds, and it would have caught the scorer's false headline without anyone reading a transcript.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 4: The Decision
&lt;/h2&gt;

&lt;h3&gt;
  
  
  05 · Which Agent Memories Should Become Runtime Checks
&lt;/h3&gt;

&lt;p&gt;Four conditions, all of which should hold.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The rule has an action predicate
&lt;/h4&gt;

&lt;p&gt;Something mechanical can decide whether an action violates it. "Do not hand-edit &lt;code&gt;config/limits.conf&lt;/code&gt;; it is generated" has one: flag any change to the file, or regenerate it and compare with the committed version. "Use &lt;code&gt;format_currency()&lt;/code&gt;, not &lt;code&gt;format_currency_legacy()&lt;/code&gt;" has one: a lint rule. "Deploys to staging go through the queue file, because the reconciler reverts direct deploys" has a weaker one: detect the deploy script at execution position. "The reconciler reverts direct deploys" has none that can run inside a session. It is a fact about a process the agent cannot observe, and it has to stay advice.&lt;/p&gt;

&lt;p&gt;Deterministic, Reproducible, and Wrong calls this class TOLD, writing its class labels in capitals: facts that are "uncorroborable: nothing in the workspace records them, the fact enters once from the user." The report draws the consequence as well: where a claim can be compiled into a check, enforcement answers the compliance problem completely, and TOLD facts are the claims that cannot be compiled, because there is nothing to check them against. The deploy memory shows that one note can hold both kinds. Its prescriptive half, use the queue, has a predicate. Its causal half, the reconciler reverts direct deploys, does not.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. The violation is silent
&lt;/h4&gt;

&lt;p&gt;If breaking the rule produces an immediate, visible failure, a compile error or a red test, the environment already enforces it, and a check adds cost without adding signal. Checks earn their place where the failure is invisible: a reverted config, a green test that could not see the change, a deploy that looks successful and is undone later.&lt;/p&gt;

&lt;p&gt;A loud but misleading failure counts as silent. A flaky test's random red is visible, but it points at the wrong cause and says nothing about a rule being broken. The same rule can also land on either side depending on the project. "Never run the tests with the system interpreter" announces itself if the missing package fails at import. If the suite guards that package with &lt;code&gt;pytest.importorskip&lt;/code&gt;, the system interpreter produces a green run that quietly tested less, and the violation is silent.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. The claim is trustworthy enough to enforce
&lt;/h4&gt;

&lt;p&gt;Enforcement amplifies whatever it enforces, including mistakes. A rule the user stated, or one verified against the workspace, can be compiled. A rule the agent inferred from one observation should not be, because compiling it launders a guess into policy. &lt;a href="https://swapnanilsaha.com/blog/a-guess-becomes-a-fact/" rel="noopener noreferrer"&gt;A Guess Becomes a Fact on the Next Read&lt;/a&gt; describes the same laundering for plain notes; a check does it with a gate attached. The trust classes in &lt;a href="https://swapnanilsaha.com/blog/agent-memory-trust-ladder/" rel="noopener noreferrer"&gt;Agent Memory Needs a Trust Ladder&lt;/a&gt; are the input this condition needs, and they have to be recorded when the memory is written, because they cannot be recovered afterwards.&lt;/p&gt;

&lt;p&gt;TRACE takes its rules only from user corrections, which puts every rule on the right side of this line at its source. There is still one hop between the user and the check: the rule text and its applicability condition are a model's rewrite of what the user said. In the F2 example the user's words were about what is left behind when a project closes, and the compiled applicability condition is about the moment a log file is written. Whether a rewrite keeps the user's meaning is itself a claim, and keeping the user's words next to the check is what makes it possible to find out.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. The reason is anchored to something observable
&lt;/h4&gt;

&lt;p&gt;Every rule exists because some condition held when it was written: the file is generated, the interpreter lacks a package, the old API is deprecated. If that condition can be tied to an artifact (the generator script, the dependency manifest, the deprecation marker), the check can be flagged automatically when the artifact changes. The anchor belongs on the condition, not on the rule. A Directive Is Not a Memory points out that anchoring a rule to a file is decorative, because the rule was never a claim about that file. "&lt;code&gt;config/limits.conf&lt;/code&gt; is generated by &lt;code&gt;tools/gen_limits.py&lt;/code&gt;" is a claim about those two files, so a change to either is evidence. If no artifact carries the condition, the check needs a named human owner and a review date, or it will outlive its reason.&lt;/p&gt;

&lt;p&gt;Memories that fail any of these stay advice. That covers most of what an agent learns: explanations, locations, design history, preferences with exceptions, and facts about systems outside the workspace.&lt;/p&gt;

&lt;h3&gt;
  
  
  06 · The Middle Rung: A Memory Note That Carries a Verify Command
&lt;/h3&gt;

&lt;p&gt;Between advice and enforcement there is a rung that keeps something of both: &lt;strong&gt;advice that carries its own verification.&lt;/strong&gt; The note does not demand belief and blocks nothing. It offers a one-command check the agent can run to confirm the claim, together with where the claim came from and when it was last confirmed.&lt;/p&gt;

&lt;p&gt;"Hand-editing &lt;code&gt;config/limits.conf&lt;/code&gt; is reverted; to confirm, regenerate it into a scratch file with &lt;code&gt;tools/gen_limits.py&lt;/code&gt; and diff the two" is still advice. But it turns the trust question into an observation the agent makes itself, so the judgment that rejected the bare note now has evidence to weigh. It also preserves the test. The agent runs the check, and if the rule no longer holds, the check says so. In the configuration scenario the agent went looking for that evidence on its own. A verify command hands it over instead of hoping the agent goes looking. It has to be safe to run, though: read-only, or writing only to a scratch path, because an agent that runs it halfway through an edit should not lose the edit to a regenerated file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/blog/agents-over-trust-instructions-and-under-trust-corrections/" rel="noopener noreferrer"&gt;Agents Over-Trust Instructions and Under-Trust Corrections&lt;/a&gt; lays out these corroboration affordances (an origin, dates, a verify hint, a content anchor) and why honest claims can carry them while planted ones cannot. Whether they change behaviour is an open question. My own pilot was designed to test this and could not answer it, and &lt;a href="https://swapnanilsaha.com/research/under-trusted-memory/" rel="noopener noreferrer"&gt;Deterministic, Reproducible, and Wrong&lt;/a&gt; reports why. The report also records a bound that no sample size removes: a check on a claim's content can only be built where the workspace records the fact. The flaky test shows the limit in practice. Some claims have no cheap verification, and for those this rung is empty.&lt;/p&gt;

&lt;p&gt;The full ladder, from least to most forceful, is my own synthesis rather than a published taxonomy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Needs belief?&lt;/th&gt;
&lt;th&gt;Preserves the test?&lt;/th&gt;
&lt;th&gt;Fits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Note on request&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;reference material, history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivered note&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;told facts with no predicate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivered note with a verify command&lt;/td&gt;
&lt;td&gt;partly: evidence replaces trust&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;checkable claims where a block would be premature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warning at the action, non-blocking&lt;/td&gt;
&lt;td&gt;yes, at the moment of action&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;high-cost actions with a detectable signature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocking check before the action or before completion&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no, unless a scoped override exists&lt;/td&gt;
&lt;td&gt;trusted, silent-failure rules with an observable anchor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;TRACE's enforcement tiers sit on the same ladder. Its intent-level rules are reminders fired when a task matches, close to the delivered-note rung; its deterministic and semantic rules block. In the paper's deployed library, every rule carried the blocking marker.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For the flaky test, my own choice would be the warning at the action plus a flaky marker in the test file itself, which gives the rule the anchor it lacked: when someone fixes the test and deletes the marker, the anchor changes and the warning is re-examined with it. The option I would rule out is a block with no anchor and no owner, which is the one a system that compiles every correction would produce.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive · Demo 2: Advice or Check?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pick a memory and see where the four conditions put it on the ladder above, from a note on request to a blocking check, and which condition decided it. Then flip the conditions one at a time to see which one is doing the work. The flaky test and the deploy rule are the instructive cases: trusted, silent, checkable at the action, and still without anything that would say when their reason is gone. The flaky-test preset lands on a warning at the action, with a block defensible only given a named owner and a review date; the generated config and the deprecated function are the two presets that compile to a blocking check. (The interactive version runs on the original page.)&lt;/p&gt;

&lt;p&gt;The decision order follows the four conditions: no predicate means advice; a loud failure means the environment already enforces it; an untrusted source means evidence before policy; no anchor means warn, or block only with an owner and a review date. The ladder is a synthesis, not a published taxonomy, and each preset's defaults are judgment calls stated in its description.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 5: The Artifact
&lt;/h2&gt;

&lt;h3&gt;
  
  
  07 · What a Compiled Check Must Carry to Stay Retirable
&lt;/h3&gt;

&lt;p&gt;If a memory does become a check, the check should keep what the memory knew, not just the rule.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The condition, not only the rule.&lt;/strong&gt; The rule is an imperative. The condition ("this file is generated by that script") is a proposition that is true or false today. Store it in a form that can be evaluated, and evaluate it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The anchor.&lt;/strong&gt; A content hash of each artifact the condition depends on. When a hash changes, flag the check as possibly stale instead of enforcing it silently. A changed anchor means the reason may have gone, never that it has, which is how &lt;a href="https://swapnanilsaha.com/blog/agent-memory-trust-ladder/" rel="noopener noreferrer"&gt;Agent Memory Needs a Trust Ladder&lt;/a&gt; treats staleness for notes; vectr, for one, flags a note whose anchored file has changed and keeps delivering it with the flag attached rather than dropping it. For a check, the flag should trigger an evaluation of the condition: re-arm with the new hash if it still holds, suspend and escalate if it does not, and fall back to a warning if it cannot be evaluated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The provenance.&lt;/strong&gt; Who stated the rule, in what words, and when, and what turned those words into a predicate. A check that blocks correct work can then be traced to its source and argued with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A failure message that teaches.&lt;/strong&gt; When the check fails, its message is the memory. "Blocked: &lt;code&gt;config/limits.conf&lt;/code&gt; is generated by &lt;code&gt;tools/gen_limits.py&lt;/code&gt;; edit the generator's input instead" does the job the note was supposed to do, at the moment it matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A logged override.&lt;/strong&gt; A scoped way to take the forbidden action deliberately, which records what happened. Each logged override is a re-test, the r in the stale-share formula. Where the forbidden action is irreversible, the override is sandbox-only or absent, and the check should declare itself untestable by design rather than look like the others.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# illustrative: the hash, dates, words and generator flag are made up&lt;/span&gt;
&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;limits-conf-is-generated&lt;/span&gt;
&lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Do not hand-edit config/limits.conf.&lt;/span&gt;
&lt;span class="na"&gt;predicate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;the agent's diff touches config/limits.conf&lt;/span&gt;
&lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config/limits.conf is generated by tools/gen_limits.py&lt;/span&gt;
  &lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python tools/gen_limits.py -o /tmp/limits.conf &amp;amp;&amp;amp; diff -q /tmp/limits.conf config/limits.conf&lt;/span&gt;
  &lt;span class="na"&gt;anchors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tools/gen_limits.py&lt;/span&gt;
      &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;9c41e0d27b3a8f15&lt;/span&gt;
  &lt;span class="na"&gt;last_confirmed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-09-12&lt;/span&gt;
&lt;span class="na"&gt;provenance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stated_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
  &lt;span class="na"&gt;words&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limits.conf&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;gets&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;regenerated,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;never&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;edit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;it&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hand"&lt;/span&gt;
  &lt;span class="na"&gt;stated_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-08-30&lt;/span&gt;
  &lt;span class="na"&gt;compiled_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;model, confirmed by user&lt;/span&gt;
&lt;span class="na"&gt;on_violation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="s"&gt;Blocked: config/limits.conf is generated by tools/gen_limits.py,&lt;/span&gt;
  &lt;span class="s"&gt;so a hand edit is reverted on the next regeneration. Edit the&lt;/span&gt;
  &lt;span class="s"&gt;generator's input instead. To confirm, run the verify command.&lt;/span&gt;
&lt;span class="na"&gt;on_anchor_change&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run verify; re-arm if it passes, suspend and flag if not&lt;/span&gt;
&lt;span class="na"&gt;override&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;allowed_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sandbox&lt;/span&gt;
  &lt;span class="na"&gt;logged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;last_override&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of the record is ordinary configuration. The fields that keep it retirable, the condition, the anchors and the override, are the ones the enforcement loop never reads, which is why they have to be required rather than left to whoever compiles the rule. To be clear about what exists: vectr stores notes and flags them; it does not compile anything into checks, and the record above is a design, not something I have shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 6: Predictions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  08 · What This Predicts About Enforcement in Agent Memory
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prediction 1.&lt;/strong&gt; For claims with a cheap verification command, a note carrying that command will be followed at a rate close to a blocking check's, without the check's cost to testability. For claims without one, only a check will change behaviour reliably. The measurement is compliance by rung on matched scenarios, split by whether a one-command verification exists. The runs with planted notes point this way at one run per cell, which is a direction and not a result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 2.&lt;/strong&gt; Checks without anchors will accumulate. The set of enforced rules will only grow, apart from the rules a user happens to contradict, and a rising fraction will block actions whose original reason no longer holds. Checks with anchors will be flagged when their reason changes, and that fraction will be much smaller, though it will still rise unless something re-tests what the anchors miss. The measurement: take a long-lived rule library, evaluate each rule's condition today, and split the stale share by whether the rule was anchored and whether it was ever overridden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 3.&lt;/strong&gt; Compiling low-provenance memories (agent-inferred, unverified) into checks will produce more blocked-correct-action events than compiling user-stated or verified ones. The measurement is the rate at which enforced rules are overridden or removed, split by the provenance of the memory they came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Line to Keep
&lt;/h2&gt;

&lt;p&gt;Advice fails when the agent does not believe it, and in the runs with planted notes, what moved the agent was evidence it could check, not wording. A check removes the need for belief, and with it the only route by which a rule ever met a counterexample. Every blocking check is a bet that its reason will hold for as long as the check is enforced.&lt;/p&gt;

&lt;p&gt;Compile a memory into a check when it has a predicate, fails silently, comes from a trustworthy source, and is anchored to something that will say when its reason is gone, and give the check a logged way to be broken on purpose. Otherwise give the agent the check to run, not the check to obey.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glossary
&lt;/h2&gt;

&lt;p&gt;Short definitions for terms used above.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Harness&lt;/strong&gt;: The software around a model that runs the agent loop: it sends prompts, executes tool calls, and can add text to what the model sees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hook&lt;/strong&gt;: A command the agent runtime runs automatically at a fixed point, such as before a tool call or at the end of a task. Its output can allow, block or annotate the event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TRACE&lt;/strong&gt;: Test-time Rule Acquisition and Compiled Enforcement (Zhou et al., 2026): mines corrections from a user's chats with a coding agent and compiles them into runtime checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic rule&lt;/strong&gt;: A rule that states one preference with one applicability condition, so it can be checked, updated or retired on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ClawArena&lt;/strong&gt;: A benchmark of multi-round coding-agent tasks in changing environments, each with an objective checker (Ji et al., 2026).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In distribution, out of distribution&lt;/strong&gt;: Test tasks drawn from the same task families the system learned from, or from families it never saw during training.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mem0&lt;/strong&gt;: An open-source long-term memory layer for LLM applications that extracts facts from conversations, stores them, and retrieves them into later prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MemoryArena&lt;/strong&gt;: A benchmark of multi-session agent tasks whose later sessions depend on what earlier sessions learned (He et al., 2026).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;F1&lt;/strong&gt;: The harmonic mean of precision and recall: one score that is high only when both are high.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual environment&lt;/strong&gt;: An isolated Python installation for one project, with its own interpreter and packages, separate from the system-wide Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI&lt;/strong&gt;: Continuous integration: a service that builds and tests every change to a repository automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selective labels&lt;/strong&gt;: Data where an outcome is recorded only for cases an earlier decision let through, so the outcomes that would judge that decision are missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Greedy&lt;/strong&gt;: The action with the highest estimated value right now. Taking it is called exploiting; taking any other action to learn more about it is exploring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nonstationary&lt;/strong&gt;: A problem whose underlying values change over time, so an estimate that was right once can drift out of date without any new evidence arriving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sandbox&lt;/strong&gt;: An isolated environment where an action can run without touching the real system, so its effects can be observed and thrown away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overloading comparison operators&lt;/strong&gt;: Redefining what an operator such as == does for a type. A value whose == always returns True makes every equality assertion pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Special-casing&lt;/strong&gt;: Code that detects the exact inputs a test uses and returns the expected answers for them, without solving the general problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regular expressions&lt;/strong&gt;: A pattern language for matching text. It matches characters, not meaning, so it cannot tell a command that runs a file from one that only names it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution position&lt;/strong&gt;: The place in a shell command where a program is actually invoked: the start, or just after a separator such as ; or &amp;amp;&amp;amp; or a pipe, past wrappers like env and timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lint rule&lt;/strong&gt;: An automated check over source code that flags a pattern, such as a call to a deprecated function, without running the program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciler&lt;/strong&gt;: A background process that repeatedly compares a system's actual state with a declared desired state and reverts any difference it finds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pytest.importorskip&lt;/code&gt;&lt;/strong&gt;: A pytest helper that imports a module, or skips the current test if the module cannot be imported, instead of failing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content hash&lt;/strong&gt;: A short fingerprint computed from a file's bytes. Any change to the file changes the hash, so comparing hashes detects edits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Zhou, Y., Guo, K., Zhuang, H., Wang, X., Huang, Y., Liang, Z., Chen, P.-Y., Gao, T., Moniz, N., Chawla, N. V., Zhang, X. &lt;a href="https://arxiv.org/abs/2606.13174" rel="noopener noreferrer"&gt;Getting Better at Working With You: Compiling User Corrections into Runtime Enforcement for Coding Agents&lt;/a&gt;. arXiv:2606.13174, 2026. TRACE: the three-part compiled rule and bounded retries (Section 4.3), the frozen evaluation and the ClawArena and MemoryArena-derived results (Section 5), the representation diagnostic (Section 3.2), the lifecycle resolver (Appendix C.4), and rule F2 (Appendix C.5).&lt;/li&gt;
&lt;li&gt;Saha, S. &lt;a href="https://swapnanilsaha.com/research/under-trusted-memory/" rel="noopener noreferrer"&gt;Deterministic, Reproducible, and Wrong: How Our Own Scorer Manufactured an Agent-Memory Finding, and the Guard That Would Have Caught It&lt;/a&gt;. Technical report, 2026. doi:10.5281/zenodo.22079952. Claim C4 on corroboration affordances, the TOLD class with its causal and prescriptive halves, the bound on affordances for uncorroborable claims, the security-regression point, the &lt;code&gt;cat deploy.sh&lt;/code&gt; scorer defect, execution-position matching and the contradiction guard.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/injection_utility/README.md" rel="noopener noreferrer"&gt;vectr, &lt;code&gt;benchmarks/injection_utility/README.md&lt;/code&gt;&lt;/a&gt;. The three trap scenarios and mechanical scoring, the delivery failures and their fixes, the decline citing no user origin and no corroboration, the wrapper and framing variants, the hook-channel arm, and the recheck after the fixes at two of three, one run per cell.&lt;/li&gt;
&lt;li&gt;vectr, &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/working_context_store/_store.py" rel="noopener noreferrer"&gt;&lt;code&gt;agent/working_context_store/_store.py&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;check_staleness()&lt;/code&gt;. A drifted anchor on a note surfaces as a visible stale flag and is never a silent drop.&lt;/li&gt;
&lt;li&gt;Lakkaraju, H., Kleinberg, J., Leskovec, J., Ludwig, J., Mullainathan, S. "The Selective Labels Problem: Evaluating Algorithmic Predictions in the Presence of Unobservables." &lt;em&gt;KDD 2017&lt;/em&gt;. &lt;a href="https://doi.org/10.1145/3097983.3098066" rel="noopener noreferrer"&gt;doi:10.1145/3097983.3098066&lt;/a&gt;; open copy at &lt;a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC5958915/" rel="noopener noreferrer"&gt;PMC5958915&lt;/a&gt;. Selectively labelled data, with bail decisions as the example.&lt;/li&gt;
&lt;li&gt;Sutton, R. S., Barto, A. G. &lt;a href="http://incompleteideas.net/book/the-book-2nd.html" rel="noopener noreferrer"&gt;&lt;em&gt;Reinforcement Learning: An Introduction&lt;/em&gt;&lt;/a&gt;, 2nd ed. MIT Press, 2018. The exploration and exploitation trade, and why a nonstationary problem needs exploration even when rewards are deterministic (Section 2.3).&lt;/li&gt;
&lt;li&gt;Zhong, Z., Raghunathan, A., Carlini, N. &lt;a href="https://arxiv.org/abs/2510.20270" rel="noopener noreferrer"&gt;ImpossibleBench: Measuring LLMs' Propensity of Exploiting Test Cases&lt;/a&gt;. arXiv:2510.20270, 2025. Test modification, operator overloading, state recording and special-casing on tasks whose tests contradict their specification, and the effect of read-only tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/once-is-too-few-twice-is-noise/" rel="noopener noreferrer"&gt;Once Is Too Few, Twice Is Noise&lt;/a&gt;: A delivered memory has a budget and a deadline: repeat it and it becomes noise, cool it down and its one delivery has to land before the decision it is about.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/a-directive-is-not-a-memory/" rel="noopener noreferrer"&gt;A Directive Is Not a Memory&lt;/a&gt;: Why a procedural note should store the checkable condition behind a rule, not only the rule, if anyone is ever going to test it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/agents-over-trust-instructions-and-under-trust-corrections/" rel="noopener noreferrer"&gt;Agents Over-Trust Instructions and Under-Trust Corrections&lt;/a&gt;: Why a true correction loses to a training prior, and why naming the overridden belief and attaching a verify command beat louder wording.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/a-guess-becomes-a-fact/" rel="noopener noreferrer"&gt;A Guess Becomes a Fact on the Next Read&lt;/a&gt;: Why a note's kind (the user's instruction, a tool result, a file fact or the model's own guess) has to be stamped at write time, before a guess is read back as fact.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agentmemory</category>
      <category>llmagents</category>
      <category>guardrails</category>
      <category>verification</category>
    </item>
    <item>
      <title>A Lesson Is a Failure Plus the Change That Fixed It: Surprise as the Write Signal for Agent Memory</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:08:30 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/a-lesson-is-a-failure-plus-the-change-that-fixed-it-surprise-as-the-write-signal-for-agent-memory-p55</link>
      <guid>https://dev.to/swapnanilsaha/a-lesson-is-a-failure-plus-the-change-that-fixed-it-surprise-as-the-write-signal-for-agent-memory-p55</guid>
      <description>&lt;p&gt;&lt;em&gt;Agent memory writes what the model decides to save, or it writes everything. Animals and people learn on surprise instead. For a coding agent the surprise has a mechanical shape: a command failed, something changed, and the same command passed. Without the change, it was a retry.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every agent memory system has to settle what gets written before it can settle anything else. The usual answers are "whatever the model decides to save" and "everything, summarized later". Models rarely volunteer a save, so the first under-fires. Most of a session is routine, so the second buries the few moments that mattered under a pile of ones that did not.&lt;/p&gt;

&lt;p&gt;Animals and people are selective in another way. How much they learn from an event tracks &lt;strong&gt;surprise&lt;/strong&gt;, the gap between what they expected and what happened. An outcome that was fully expected teaches close to nothing, even on the hundredth repetition.&lt;/p&gt;

&lt;p&gt;For a coding agent, the surprise worth keeping has a mechanical shape: &lt;strong&gt;a command failed, something changed, and the same command then passed.&lt;/strong&gt; The lesson is the middle element. Take it away and what is left is a retry that happened to pass, and on real build systems those are common enough that a memory unable to tell the difference fills up with luck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The Principle
&lt;/h2&gt;

&lt;h3&gt;
  
  
  01 · Learning Runs on Prediction Error, Not on Repetition
&lt;/h3&gt;

&lt;p&gt;The idea that learning tracks surprise came out of a conditioning result that repetition could not explain.&lt;/p&gt;

&lt;p&gt;In a set of experiments published in the late 1960s, Leon Kamin first trained rats that one cue, call it A, reliably came before a mild shock. Then he presented A together with a second cue, B, followed by the same shock, many times over. Tested alone afterwards, B produced almost no conditioned response, even though it had been paired with the shock as often as it was in a control group that got only the A-plus-B stage and learned B well. The effect is called blocking. Kamin read it in terms of surprise: by the second stage the shock was already predicted by A, so B's arrival added nothing, and nothing was learned about it. The chapter that set out the argument is titled "Predictability, surprise, attention, and conditioning".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The dashboard nobody learns to read&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A colleague on your team always messages you when the nightly build breaks, usually before anything else could tell you. Later someone adds a dashboard tile that turns red on the same failures. The tile is right every time, and you see it every morning, and you never learn what it means. You never needed to. Every time it went red, you already knew. The tile is B. Your colleague is A.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Robert Rescorla and Allan Wagner turned that reading into a learning rule in 1972. The amount an association strengthens on a trial is proportional to the difference between the outcome that occurred and the outcome predicted, and the prediction comes from &lt;em&gt;all&lt;/em&gt; the cues present on the trial, not from each cue separately. In Rescorla's own later summary: "associative learning occurs not because two events co-occur but because that co-occurrence is unanticipated on the basis of current associative strength."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Rescorla-Wagner rule&lt;/strong&gt;&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ΔV_X = α_X · β · (λ − ΣV)
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;V_X is the associative strength of cue X: how strongly it has come to predict the outcome. ΣV is the sum of the strengths of every cue present on the trial: the prediction. λ is the most the outcome can support, 1 if it occurs and 0 if it does not. α_X and β are learning rates set by how salient the cue and the outcome are. The bracket is the prediction error.&lt;/p&gt;

&lt;p&gt;Once A has been trained, V_A is close to λ, so on an A-plus-B trial the error λ − V_A − V_B is close to zero and B gains almost nothing. A fully predicted outcome teaches nothing, however many times it occurs. Rescorla points out that the rule is essentially the Widrow-Hoff learning algorithm, the delta rule from 1960s adaptive signal processing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model is not the last word. Its own author lists shortcomings: it predicts that a cue signalling the outcome's absence should lose that meaning when shown on its own, which does not happen, and it says nothing about timing within a trial. Mackintosh (1975) and Pearce and Hall (1980) built rival theories in which surprise and predictiveness change how much attention a cue receives, and the three accounts still disagree about why blocking happens. None of them went back to counting co-occurrences.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive · Demo 1: Paired With the Outcome, and Learned Almost Nothing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Train cue A alone first, then A and B together, and watch B's strength and the prediction error on every trial. B is paired with the outcome on every compound trial (A and B together) and barely moves, because by then the outcome is not a surprise. Set the first stage to zero trials to see what B learns when it arrives while the outcome is still unpredicted. With the defaults (15 trials of A alone, then 12 of A and B, learning rate 0.25), B ends at about 1% of what it learns in a control that skips the first stage. (The interactive version runs on the original page.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  The same error signal in dopamine neurons
&lt;/h4&gt;

&lt;p&gt;The quantity turned up in the brain. Wolfram Schultz's laboratory recorded dopamine neurons in monkeys while the animals learned that a cue predicted a drop of juice. Before learning, the neurons fired when the juice arrived. After learning, they fired to the cue and not to the juice. When the predicted juice failed to arrive, their firing dipped below baseline at the moment it had been due. Schultz, Dayan and Montague (1997) put it this way: the neurons emit "a positive signal (increased spike production) if an appetitive event is better than predicted, no signal (no change in spike production) if an appetitive event occurs as predicted, and a negative signal (decreased spike production) if an appetitive event is worse than predicted." An appetitive event is a rewarding one, like the juice.&lt;/p&gt;

&lt;p&gt;The paper's claim is that this pattern is the error term of temporal-difference learning, δ(t) = r(t) + γV(t+1) − V(t): the reward just received, plus the prediction from the next moment discounted by γ (a number a little below 1, so later reward counts for less), minus the prediction from this one. The authors present it as a generalization of Rescorla-Wagner that predicts the sum of future rewards rather than a single outcome. What the recordings show is the sign pattern: better than predicted, as predicted, worse than predicted. They do not show that the brain runs this exact algorithm, and the paper does not claim they do.&lt;/p&gt;

&lt;h4&gt;
  
  
  Hypercorrection: confident errors are the ones that get fixed
&lt;/h4&gt;

&lt;p&gt;In human memory, the hypercorrection effect shows the principle from the other side. Butterfield and Metcalfe (2001) had people answer general-knowledge questions and rate their confidence in each answer, showed them the correct answer after every error, and retested them later. The errors made with the highest confidence were the ones most likely to be corrected on the retest. That is the opposite of what a strength-based account predicts, since a confident answer should be the hardest to overwrite.&lt;/p&gt;

&lt;p&gt;Metcalfe's 2017 review names two factors, and says they are not mutually exclusive. One is surprise: feedback that contradicts a confident answer captures attention. In a follow-up, people doing the task while listening for very soft tones missed more of the tones played at the moment they got feedback on a high-confidence error, and EEG recordings show a larger surprise-related response to that feedback. The other is knowledge: people tend to make confident errors in domains they know well, where the right answer is often already familiar once they see it. Both probably contribute. Surprise does not have to be the whole story to be a reliable marker of where learning happens.&lt;/p&gt;

&lt;h4&gt;
  
  
  Incidental learning: retention as a side effect
&lt;/h4&gt;

&lt;p&gt;People also do not have to decide to remember for learning to happen. Hyde and Jenkins (1973) had groups of students listen to 24 words while doing one of five orienting tasks: rating each word's pleasantness, estimating how often it is used in English, checking whether it contains an E or a G, naming its part of speech, or judging whether it fits a sentence frame. Half the groups were told to expect a recall test and half were not, and two control groups did no task and simply tried to learn the list. Hyde and Jenkins counted the first two as semantic, since they require thinking about what a word means, and those two produced far more recall than the other three. The control groups, which only tried to memorize, did no better than the semantic-task groups that had been told about the test, and across this line of experiments a meaning-based task has produced about as much recall as a deliberate attempt to learn. Knowing that a test was coming made some difference. Which task people did made much more. Retention followed from what people did with each word.&lt;/p&gt;

&lt;h4&gt;
  
  
  Model architectures reach for the same signal
&lt;/h4&gt;

&lt;p&gt;At least one recent architecture uses the principle directly. Titans (Behrouz, Zhong and Mirrokni, 2025) adds a neural long-term memory that keeps learning while the model runs, and scales each write by how surprising the input is, measured as the gradient of the memory's loss on that input. In its simplest form the update is M_t = M_(t−1) − θ&lt;em&gt;t ∇ℓ(M&lt;/em&gt;(t−1); x_t), where x_t is the new input, ℓ is how badly the memory M predicts it, and θ_t is a learning rate. The full version adds a forgetting gate and a momentum term, and the momentum carries recent surprise forward, so the inputs just after a surprising one are also written strongly. The paper motivates the design with the same observation from psychology, that an event which violates expectations is more memorable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Two quantities, not one&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The error term in Rescorla-Wagner says &lt;em&gt;how much&lt;/em&gt; to learn on a trial. The set of cues present says &lt;em&gt;what&lt;/em&gt; gets the credit. Blocking is a statement about credit: the outcome had already been credited to A, so B got none. A memory that writes on surprise needs both parts. The surprise says when to write. Something else has to say what the write is about.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For agent memory, this points at a specific design. &lt;strong&gt;The write signal should be a measurable surprise that the harness detects, rather than a save the model decides to make.&lt;/strong&gt; The case against waiting for the model is empirical, and it is laid out in &lt;a href="https://swapnanilsaha.com/blog/agent-memory-harness-property/" rel="noopener noreferrer"&gt;The Agent Never Chooses to Remember: Memory as a Harness Property&lt;/a&gt;: an agent given memory tools in a seeded run never chose to use them. The harness, the software that runs the agent loop, sees every tool call anyway, so it is the natural place to look for surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: The Signal
&lt;/h2&gt;

&lt;h3&gt;
  
  
  02 · What Surprise Looks Like in a Coding Agent's Tool Stream
&lt;/h3&gt;

&lt;p&gt;A coding agent's session is a stream of tool calls: shell commands with exit statuses and output, file reads, file edits. Most of it is unsurprising. A search returns results, a file is read, a test that was expected to pass passes.&lt;/p&gt;

&lt;p&gt;The surprising moment has a shape a harness can detect without asking a model anything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A command runs and &lt;strong&gt;fails&lt;/strong&gt;: a non-zero exit status, or output containing an unambiguous failure marker.&lt;/li&gt;
&lt;li&gt;Something &lt;strong&gt;changes&lt;/strong&gt;: the command's own arguments or flags, a file the agent edits, or the environment the command runs in.&lt;/li&gt;
&lt;li&gt;The same command, or a close variant, runs again in the same directory and &lt;strong&gt;passes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That sequence is a discovery. At step 1 the agent did not know something; by step 3 it did, and the evidence is the transition from red to green. What it learned is the change in step 2, and it only makes sense bound to the context of step 1. In Rescorla-Wagner terms, the failure is the error that says a write should happen, and the change is the cue that gets the credit.&lt;/p&gt;

&lt;p&gt;A harness cannot see what the model expected, so a failure is a proxy for surprise rather than surprise itself. Usually it is a good one, because an agent runs a command in the expectation that it will work. The exception is a reproduction run, where the agent expects a test to fail. There a red is the expected result and a green would be the surprise, and a detector that keys on failures cannot see that kind at all.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where this sits next to "the first search is the write"
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/blog/search-is-memorys-write-path/" rel="noopener noreferrer"&gt;Search Is Memory's Write Path&lt;/a&gt; argued that for knowledge about a codebase, the first search is the write and every repeat is the read, and that capture is the hard half, because the conclusion a search produces lives in the model's reasoning rather than in the tool stream. Which moments deserve a write is a separate question. For one class of knowledge the answer is entirely in the tool stream. The failing invocation, the change and the passing invocation are all tool calls, so capturing a fail, change, pass sequence needs no model judgment at all.&lt;/p&gt;

&lt;h4&gt;
  
  
  A lesson is a (context, change) pair
&lt;/h4&gt;

&lt;p&gt;The framing has precedent one level up. Research on mining reusable bug-fix patterns from version history treats a pattern as a pair: what was wrong, and what edit fixed it. FixMiner (Koyuncu et al., 2020) mines fix patterns from what it calls Rich Edit Scripts, edit scripts that carry "the AST-level context of the code changes", and feeds them to an automated program repair tool. The fixed code on its own would not do, because the fixed artifact without the broken one does not say what the fix &lt;em&gt;was&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Commands work the same way. Storing "&lt;code&gt;npm install --legacy-peer-deps&lt;/code&gt; works" loses the lesson. The lesson is "&lt;code&gt;npm install&lt;/code&gt; fails here with a peer-dependency conflict, and adding &lt;code&gt;--legacy-peer-deps&lt;/code&gt; gets past it". The context also bounds the lesson. That flag tells npm to stop enforcing peer-dependency requirements, which is a reasonable workaround in one repository and a way to ship a broken install in another. A rule stripped of the failure that motivated it cannot tell which repository it is in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: The Change
&lt;/h2&gt;

&lt;h3&gt;
  
  
  03 · Why the Change Separates a Lesson From a Retry
&lt;/h3&gt;

&lt;p&gt;The simplest detector would be: a command failed, later it passed, so record it. That detector is wrong often enough to be dangerous.&lt;/p&gt;

&lt;p&gt;Durieux, Le Goues, Hilton and Abreu (2020) studied restarted builds on Travis CI, across 3,286,773 builds collected over about ten weeks in late 2019. Developers "restart at least 1.72% of builds, amounting to 56,522 restarted builds". A restart re-runs "the same test suite on the same code". Of the restarted builds that had originally failed or errored, &lt;strong&gt;46.77% passed after the restart&lt;/strong&gt;. The causes the authors identify are mostly failing tests, network problems and Travis CI limits such as execution timeouts: flaky behaviour, not code.&lt;/p&gt;

&lt;p&gt;The 46.77% is not the chance that an arbitrary failing build passes on a rerun. Developers chose which builds to restart, and they restart the ones they suspect; the authors note that developers seem "reasonably adept at identifying such flaky builds." That selection is the relevant one here, though. An agent that reruns a command without changing anything is making the same bet, that the failure was not its fault. When the bet pays off, the only thing learned is that the command is unreliable. A memory that records the flip as a lesson teaches the next session something false: that some action was the fix, when nothing was.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How much of a naive memory is luck&lt;/strong&gt;&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;F = genuine fixes (fail, change, pass) in some period
R = failures the agent reran unchanged
q = probability that an unchanged rerun passes
naive detector records   F + q·R  "lessons"
fraction that are luck = q·R / (F + q·R)
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Take q from the Travis figure, 0.4677, and assume one unchanged rerun for every genuine fix (R = F). Then 0.4677 / 1.4677 ≈ 0.32: about a third of what the naive detector stores is noise, and no single record says which third. Neither input has been measured for agents; they show the shape. The noise share grows with how often the agent reruns without changing anything and with how flaky its commands are, and it cannot be seen one record at a time.&lt;/p&gt;

&lt;p&gt;Requiring a change removes most of the q·R term. What survives is an unchanged failure that happens to clear right after an unrelated change. The cost runs the other way: a fix whose change the harness cannot see is missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fever that breaks on its own&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A doctor who writes "gave ginger tea, fever broke" in a casebook has recorded a sequence, not a cure. Many fevers break on their own. Writing only "fever broke" is worse still, because it cannot even name what to try next time. Medicine separates treatment from natural course with a control group. A command stream carries a partial control of its own: when the identical command passes on a rerun with nothing changed, the environment has just shown that this failure can clear by itself.&lt;/p&gt;

&lt;p&gt;The analogy also marks the limit of the rule. A change followed by a pass is better evidence than a pass alone, but it is not proof: the fever might have broken on the day the tea was given. A detector that requires a change inherits that weakness, and has to be designed around it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The change is what separates learning from luck&lt;/strong&gt;, so the detector has to require one. It can appear in the command, in the files, or in the environment, and the detector also needs a rule for when it appears nowhere.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The command itself changed
&lt;/h4&gt;

&lt;p&gt;The failing and passing commands must be similar enough to be the same attempt, and different enough to contain a change. For concrete numbers, take vectr, an open-source code-search and agent-memory tool I maintain. It ships a detector for these sequences, which it calls arcs, and the &lt;a href="https://github.com/swapnanil/vectr/blob/main/app/arcs.py" rel="noopener noreferrer"&gt;source&lt;/a&gt; is public; the figures that follow are its defaults. It only compares two commands if they fall in the same &lt;strong&gt;bucket&lt;/strong&gt;: the same invoked program, run in the same directory. A changed command is, in its terms, a &lt;strong&gt;mutation&lt;/strong&gt;. Each command is first normalized: the invoked program and subcommand become the verb, dash-prefixed options become flags, the rest become arguments, a leading &lt;code&gt;cd&lt;/code&gt; is stripped, and a trailing pipe into &lt;code&gt;tail&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt; or &lt;code&gt;cat&lt;/code&gt; is dropped because it changes only what a person sees.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Same attempt, different arguments&lt;/strong&gt;&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;score = 0.5 · verb + 0.3 · J(flags) + 0.2 · args
mutation  ⇔  0.55 ≤ score &amp;lt; 0.999
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;em&gt;verb&lt;/em&gt; is 1 if the verbs match, 0.7 if they are near-identical strings (a typo fix), otherwise 0. &lt;em&gt;J(flags)&lt;/em&gt; is the Jaccard similarity of the two flag sets: the size of their intersection divided by the size of their union. &lt;em&gt;args&lt;/em&gt; is the average per-position string similarity when both commands have the same number of arguments; when the counts differ, it is the overlap of argument types (paths, numbers and so on), scaled down by the gap in count. A score of exactly 1 means the normalized commands are identical, and an identical command is never a mutation: it goes to the edit-and-environment check instead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install&lt;/code&gt; followed by &lt;code&gt;npm install --legacy-peer-deps&lt;/code&gt; scores 0.5 × 1 + 0.3 × 0 + 0.2 × 1 = 0.70, inside the band. &lt;code&gt;pytest&lt;/code&gt; followed by &lt;code&gt;python -m pytest&lt;/code&gt;, which can be a real fix because it puts the current directory on Python's import path, lands in a different bucket (a different program) and would score 0.05 anyway. &lt;code&gt;cargo build&lt;/code&gt; followed by &lt;code&gt;cargo build --features tls&lt;/code&gt; scores 0.50 and falls below the band, so that fix would be missed.&lt;/p&gt;

&lt;p&gt;The score measures resemblance, not cause. A failing &lt;code&gt;pytest tests/test_api.py::test_login&lt;/code&gt; followed by a passing &lt;code&gt;pytest tests/test_api.py::test_logout&lt;/code&gt; scores 0.98 and is recorded, although running a different test fixed nothing. These weights are design defaults, not fitted values. The detector's own replay notes say none of them were tuned against real transcripts, because the only corpus available had no failures to tune against.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  2. A file was edited, or the environment changed
&lt;/h4&gt;

&lt;p&gt;Most coding repair loops never touch a flag. They go: fail, edit a source or config file, rerun the &lt;em&gt;identical&lt;/em&gt; command, pass. A mutation rule alone cannot tell that rerun from a flaky retry, since the command is identical. With edit events from the same window it becomes the strongest kind of lesson: the change is a file diff, and the edited paths are the payload.&lt;/p&gt;

&lt;p&gt;The environment is the third place a change can live, and the hardest to observe. vectr sees it only as a variable set in front of the command, as in &lt;code&gt;PYTHONPATH=src pytest ...&lt;/code&gt;, and it compares variable names, not values. An &lt;code&gt;export&lt;/code&gt; run as a separate command is invisible to it, and so is a switch from &lt;code&gt;CC=gcc&lt;/code&gt; to &lt;code&gt;CC=clang&lt;/code&gt;: both reruns look unchanged and are treated as retries.&lt;/p&gt;

&lt;p&gt;The edit rule has a weakness of its own. It checks that some file was edited in the window, not that the edit had anything to do with the failure, and it does not consult the command's flip history. A flaky test that fails, then passes after the agent fixes a typo in a README, is recorded as a lesson with the README in its payload. That is the ginger tea, and nothing at capture time catches it. It is one of the reasons to keep the raw record rather than a summary of it.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Nothing changed: suppress it, and count it
&lt;/h4&gt;

&lt;p&gt;An identical rerun that passes with no edit and no environment change is a retry, and it is never recorded as a discovery. It is counted, per command, as an unchanged flip. The count carries information of its own: this command's outcome does not depend only on its input.&lt;/p&gt;

&lt;p&gt;Build tools already draw the same line. Maven's test runner, Surefire, can rerun failing tests (&lt;code&gt;rerunFailingTestsCount&lt;/code&gt;). A test that fails and then passes on a rerun is counted as a flake, the build succeeds, and the summary reads &lt;code&gt;Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Flakes: 1&lt;/code&gt;. The tool files the rerun under instability.&lt;/p&gt;

&lt;p&gt;vectr adds one rule on top, aimed at the Travis base rate. After two unchanged flips of the same command in one session, near-identical variants of it (similarity 0.95 or more) stop producing lessons as well, since a command that flips on its own is weak evidence that a one-character tweak fixed it. The rule applies only to changed commands; the edit rule never consults it.&lt;/p&gt;

&lt;h4&gt;
  
  
  The traps, and a narrow rule for each
&lt;/h4&gt;

&lt;p&gt;Five more traps show up in real tool streams. Each gets one narrow rule. A single clever rule covering all of them would be harder to test and easier to fool.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A tool exits 0 while reporting failure.&lt;/strong&gt; The common case is a test run piped into &lt;code&gt;tail&lt;/code&gt;: a pipeline reports the exit status of its last command, and &lt;code&gt;tail&lt;/code&gt; succeeds. Rule: read failure and success markers in the output first, and trust the exit status only when no marker matches. A marker match with exit 0 is a "soft failure", and it counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A transient condition clears at the same moment as an unrelated change.&lt;/strong&gt; A network error on the first run, an edit to a README, then a pass. Rule: if the failure's output matches a known transient signature (a timeout, a connection reset, a rate limit), keep the arc but mark it low confidence. In vectr this rule is wired into the detector, but the shipped marker table does not yet define the transient signatures it refers to, so as of this writing it cannot fire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An interrupted run looks like a failure.&lt;/strong&gt; A Ctrl-C in the middle of a test run can print a partial summary with failures in it. Rule: an interrupted or signal-killed run never starts an arc and never resolves one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same command, in a different place.&lt;/strong&gt; &lt;code&gt;make test&lt;/code&gt; failing in one service and passing in another is two facts, not a fix. Rule: the directory the command actually ran in, including any leading &lt;code&gt;cd&lt;/code&gt;, is part of the bucket key, not a kind of change. A pass in one directory never resolves a failure in another. Some real fixes are a change of directory, and this rule loses them; binding unrelated services together would cost more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old failures.&lt;/strong&gt; A pass an hour later is not an answer to a failure from an hour ago. Rule: a pending failure ages out after 20 commands or 10 minutes, whichever comes first.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive · Demo 2: What Counts as a Lesson?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Choose how the first run failed, what happened between the runs, and how the second run went. The panel shows what a surprise-driven detector does with the pair, and why. Worth trying in turn: every option under "Between the runs" with a passing second run, which shows both the lessons it keeps and the two it gets wrong; the same with an interrupted first run; and "nothing" or "a neighbouring test" after two earlier unchanged flips. (The interactive version runs on the original page.)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;With a plain failing first run and a passing second run, the detector's verdicts are: an edit to &lt;code&gt;src/api/auth.py&lt;/code&gt;, an added flag, or &lt;code&gt;PYTHONPATH=src&lt;/code&gt; in front of the command each record a lesson; a README typo fix records one too, wrongly; a neighbouring test records one, also wrongly, until the command has flipped twice with no change; nothing in between is suppressed as a retry; rerunning from a subdirectory or switching to &lt;code&gt;python -m pytest&lt;/code&gt; records nothing, because the second run lands in a different bucket. A first run that was interrupted records nothing whatever follows.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 4: The Record
&lt;/h2&gt;

&lt;h3&gt;
  
  
  04 · Keep the Raw Arc, Distill the Lesson Later
&lt;/h3&gt;

&lt;p&gt;A detected arc is raw material. The obvious next move is to summarize it into a rule, "in this repo, use &lt;code&gt;--legacy-peer-deps&lt;/code&gt;", and throw the raw sequence away. The better-known systems that learn from the trajectories of LLM agents mostly keep the evidence, and the one that compresses hardest documents what compression costs.&lt;/p&gt;

&lt;h4&gt;
  
  
  ExpeL keeps failed and successful attempts side by side
&lt;/h4&gt;

&lt;p&gt;ExpeL (Zhao et al., AAAI 2024) gathers experience by trial and error, retrying failed training tasks after a written self-reflection, and puts every trajectory, failed or successful, into an experience pool. Extracting insights is a separate, later step: the model compares "a failed trajectory with a successful trajectory for the same task", or looks for patterns across several successes, and edits a running list of insights that can be upvoted, downvoted or removed. At evaluation the agent gets both the insights and the most similar successful trajectories as worked examples. A failed attempt, a reflection, then a successful retry of the same task is a fail, change, pass arc one level up, and ExpeL keeps the pair.&lt;/p&gt;

&lt;h4&gt;
  
  
  Voyager commits only what a success verified
&lt;/h4&gt;

&lt;p&gt;Voyager (Wang et al., 2023) writes code to act in Minecraft. It runs each program, feeds execution errors and environment feedback into the next round of code generation, and commits a program to its skill library only after a separate verification step confirms the task succeeded. After four rounds without success it moves on. The failing attempts stay in the prompt until then; they never enter the library.&lt;/p&gt;

&lt;p&gt;Voyager does store the fixed program without its failures, and for Voyager that is right. A skill is retrieved by an embedding of its description, when the agent has a goal the skill serves. A command lesson is needed at a different moment, when the same failure recurs, so the failure is its retrieval key and cannot be thrown away.&lt;/p&gt;

&lt;h4&gt;
  
  
  Agent Workflow Memory, and what compression dropped
&lt;/h4&gt;

&lt;p&gt;Agent Workflow Memory (Wang, Mao, Fried and Neubig, 2024) compresses hardest. It prompts a model to induce workflows from past trajectories: reusable sub-routines with example-specific values replaced by placeholders such as &lt;code&gt;{product-name}&lt;/code&gt;. In its online setting it induces them only from trajectories that a model-based evaluator judged successful, and it stores the workflows rather than the trajectories. The method improves success on two web-navigation benchmarks.&lt;/p&gt;

&lt;p&gt;The paper also tests a variant that wraps each workflow as a single callable action, and it reports that variant's failure case plainly. When booking a flight, the site pops up a list of nearby airports after a city name is typed. A book-flight workflow that enters all the required data as a pre-determined action sequence fails, because "the action to choose pop-up airports is executed without seeing the intermediate states with available pop-up options". The compressed form kept the steps and dropped the observation between them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What early distillation loses&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A summary written at capture time keeps the fix and drops the branching that explains why it worked: what the failure looked like, what state the agent saw between steps, which directory it ran in. Without that, a later session facing a variant of the same failure cannot recognize it as the same failure. Capture raw arcs deterministically and immediately. Distill them later, at a boundary such as the end of a session, with the raw arc still attached as evidence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keeping the two steps apart also makes each one's failures easier to find. Detection is deterministic: it can be tested on constructed sequences and audited line by line on real transcripts. Distillation is a judgment, made by a model, and it can be wrong. Keeping the raw arc means a wrong distillation can be checked against its evidence instead of replacing it. It also works in the other direction. The arc that credits a README typo fix for a flaky pass is a bad lesson, and a distillation step that reads the raw record can see that the only edited file was documentation and reject it. A rule written at capture time would have kept the pass and lost that detail.&lt;/p&gt;

&lt;p&gt;When and how the later pass should run is the subject of &lt;a href="https://swapnanilsaha.com/blog/consolidation-is-the-missing-phase/" rel="noopener noreferrer"&gt;Consolidation Is the Missing Phase in Agent Memory&lt;/a&gt;, which argues that the write path is the wrong vantage point for integrating, abstracting or selecting, because at write time the agent has one episode and a task to finish. Arcs add one requirement to that phase: every lesson it writes has to keep a pointer to the arcs it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 5: The Evidence
&lt;/h2&gt;

&lt;h3&gt;
  
  
  05 · Zero Arcs in Twenty Agent Sessions, and What the Environment Hid
&lt;/h3&gt;

&lt;p&gt;vectr's arc detector was replayed over 20 recorded agent sessions against Apache Camel, a large multi-module Java codebase, from two earlier benchmark runs: 246 shell commands and 9 file edits in all. It emitted &lt;strong&gt;zero arcs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The two runs were different kinds of work. Twelve sessions were read-only exploration: they answered questions about the code and made no edits, so an edit-mediated arc (one where the change is a file edit) was structurally impossible there, and a mutation arc would have needed a failing command. The other eight were bugfix sessions. They made all nine edits and ran the tests.&lt;/p&gt;

&lt;p&gt;Zero is either a detector bug or a true fact about the corpus, so every one of the 246 commands was checked by hand against the raw transcripts, outside the detector, for any sign of failure: an error flag from the tool layer, a &lt;code&gt;BUILD FAILURE&lt;/code&gt; line, a JUnit summary with failures, a Python traceback, "command not found". The tool layer flagged no command as an error. There were 35 matches on failure-shaped text, and every one sat in the output of a Maven test run that ended in &lt;code&gt;BUILD SUCCESS&lt;/code&gt;. In every bugfix session, the Maven command that followed an edit passed on its first run. No session contained a fail, edit, rerun, pass loop anywhere. Zero was the correct answer.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why no test ever failed
&lt;/h4&gt;

&lt;p&gt;That is a stranger result than it looks. Eight sessions edited source code and ran tests, and not one test run went red.&lt;/p&gt;

&lt;p&gt;The cause was the build setup. All 25 Maven invocations in the bugfix sessions were scoped to a single module. In Maven, a single-module run compiles against the previously installed copies of the other modules (the ones in the local &lt;code&gt;~/.m2&lt;/code&gt; cache), not against their source in the working tree. On the tasks where the fix lived in one module and its test in another, the agent's edit was invisible to the test it ran. The runs were also routinely quieted (Maven's &lt;code&gt;-q&lt;/code&gt; flag suppresses most output) or piped through &lt;code&gt;tail&lt;/code&gt;, which replaces Maven's exit status with &lt;code&gt;tail&lt;/code&gt;'s. One bugfix session did what a careful engineer does and ran the bug's own test before touching the code, expecting red. It got green, on code that still contained the bug.&lt;/p&gt;

&lt;p&gt;The piping is not why the detector saw nothing. It reads failure markers in the output before it trusts an exit code, precisely because pipes misreport exit status. There was no failure text to read.&lt;/p&gt;

&lt;p&gt;The detector saw nothing because the environment never produced a failure for it to see. &lt;strong&gt;A memory written on prediction error cannot learn in an environment that never reports an error.&lt;/strong&gt; Capture of this kind depends on the agent knowing how to get honest feedback from its environment: which modules to build together, which flags hide the result. That is a different kind of knowledge, procedural rather than factual, and it is the subject of &lt;a href="https://swapnanilsaha.com/blog/agents-forget-how-not-what/" rel="noopener noreferrer"&gt;Agents Forget How, Not What&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The one real surprise in the corpus was a pass&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The only moment in those eight sessions that contradicted an agent's expectation was the reproduction run that went green on buggy code. The model noticed. Its reasoning at that point reads: "Wait, the test is already passing? But the bug description says it should fail." It spent 19 of its 40 turns after that green trying to account for it and ran out of turns without writing a fix.&lt;/p&gt;

&lt;p&gt;A detector keyed on failures sees none of this. A green is a success, and successes only resolve pending failures; they never start anything. The surprise existed, but it lived in the model's reasoning, not in the tool stream. False passes are invisible to failure-driven capture by construction. Catching them needs a different trigger, such as the bug's own test passing before anything has been edited, which a harness can also detect without asking a model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  What the zero does and does not show
&lt;/h4&gt;

&lt;p&gt;It shows the detector produced no false lessons on real transcripts, including in sessions full of test runs and edits. It cannot show that the detector finds real lessons, because the corpus contains none. That half rests on synthetic tests in vectr's suite, constructed sequences that pit fail, edit, identical rerun against fail, identical rerun with no edit. How many of the real lessons in real transcripts the detector would catch is, so far, unmeasured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 6: The Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  06 · What Surprise-Driven Capture Changes in an Agent Memory Design
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Write on arcs, not on intent
&lt;/h4&gt;

&lt;p&gt;Voluntary saves stay available; a model that has something worth saying should be able to say it. But the default capture channel is a detector in the harness, watching the tool stream and requiring a failure, a change and a pass.&lt;/p&gt;

&lt;h4&gt;
  
  
  Store the pair, not the fix
&lt;/h4&gt;

&lt;p&gt;The record is the failing invocation, the observed failure, the change (an argument diff or the list of edited paths), the passing invocation, and its scope: the directory the command ran in and a coarse environment delta. A fix learned in one repository is not automatically a fix in another, and a fix learned in one service directory is not automatically a fix in its neighbour. vectr's arc record has this shape: the chain of failing runs, the passing run, a diff over five axes (verb, flags, arguments, environment, edited files) and a confidence label.&lt;/p&gt;

&lt;h4&gt;
  
  
  Suppress retries loudly
&lt;/h4&gt;

&lt;p&gt;Unchanged fail-then-pass sequences are counted as instability, never stored as lessons, and the count belongs somewhere a person or a later session can see it. "This test flipped three times today with no change" is useful to the next session: it describes the test and prescribes nothing. vectr currently keeps the count inside the detector, where it drives suppression, and does not show it to anyone yet.&lt;/p&gt;

&lt;h4&gt;
  
  
  Distill at a boundary, with the evidence attached
&lt;/h4&gt;

&lt;p&gt;A distilled lesson carries a reference to the arcs it came from, so a later reader can check it. In vectr, captured arcs wait in a review step, and a note written from them records which arcs it distilled. Whether notes written this way improve later sessions has not been measured.&lt;/p&gt;

&lt;h4&gt;
  
  
  Treat zero arcs as a signal about the environment
&lt;/h4&gt;

&lt;p&gt;A long session with edits and no failures should worry whoever reads it. It may mean the agent's tests cannot fail. In the Camel sessions, a count of arcs per session would have read zero for eight bugfix sessions in a row, and that zero would have been a cheap early sign that the tests could not fail.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Write policy&lt;/th&gt;
&lt;th&gt;What triggers a write&lt;/th&gt;
&lt;th&gt;What gets stored&lt;/th&gt;
&lt;th&gt;How it fails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Voluntary save&lt;/td&gt;
&lt;td&gt;The model decides to save&lt;/td&gt;
&lt;td&gt;The model's own summary&lt;/td&gt;
&lt;td&gt;Under-fires: the save rarely happens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Save everything&lt;/td&gt;
&lt;td&gt;Every event, or a summary at session end&lt;/td&gt;
&lt;td&gt;The transcript, or a compression of it&lt;/td&gt;
&lt;td&gt;Over-fires: routine buries the few lessons, and a summary drops the branch that explained them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arc capture&lt;/td&gt;
&lt;td&gt;Harness sees a failure, a change, then a pass&lt;/td&gt;
&lt;td&gt;Failing run, failure, change, passing run, scope&lt;/td&gt;
&lt;td&gt;Blind where nothing fails, and misses changes it cannot see, such as a switch to a different program&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The three policies can coexist. The open question is which one is the default channel.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 7: Predictions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  07 · What Surprise-Driven Agent Memory Predicts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prediction 1.&lt;/strong&gt; On real transcripts where honest failures occur, a detector that only looks for changed commands will miss most real lessons, and adding edit-mediated arcs will recover them. The measurement is the fraction of hand-labelled lessons each detector finds. The Camel replay cannot run it, because it contains no failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 2.&lt;/strong&gt; A store populated from arcs will have far fewer entries than one populated by saving everything and far more than one populated by voluntary saves, and its entries will be retrieved and used at a higher rate per entry than either. If per-entry usage is not higher, surprise is not a better write signal than intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 3.&lt;/strong&gt; Recording unchanged fail-then-pass sequences as lessons will measurably degrade later sessions, by prescribing actions that had nothing to do with the pass. This tests the consequence of the Travis base rate directly: the same memory, with and without retry suppression, on tasks that include a known flaky command.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Line to Keep
&lt;/h2&gt;

&lt;p&gt;A memory should write when the environment contradicts the agent, and what it should store is the change that resolved the contradiction, kept next to the failure. That rule is only as good as the environment's honesty. Where tests cannot fail it writes nothing, and the thing to fix is the tests.&lt;/p&gt;

&lt;p&gt;People do not decide what to remember; surprise does much of the deciding. For an agent the surprise is a command that failed and then passed, and the lesson is the change in between. Without the change it was a retry, and a memory that cannot tell the two apart teaches luck as if it were knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glossary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conditioned response&lt;/strong&gt;: A learned reaction to a cue that predicts something. In Kamin's rats, a pause in lever pressing for food when a cue that predicts shock comes on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocking&lt;/strong&gt;: When a cue that already predicts an outcome prevents a new cue, presented alongside it, from being learned as a predictor of the same outcome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Associative strength&lt;/strong&gt;: A number standing for how strongly a cue has come to predict an outcome. Zero means it predicts nothing; λ means it predicts the outcome fully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delta rule&lt;/strong&gt;: An update rule that changes each weight in proportion to the error between a target and the current output. It is the single-layer ancestor of gradient descent in neural networks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dopamine neurons&lt;/strong&gt;: Neurons in the midbrain that release the chemical messenger dopamine. They project widely and are involved in reward, movement and learning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal-difference learning&lt;/strong&gt;: A family of learning algorithms that update a prediction of future reward using the difference between successive predictions, rather than waiting for the final outcome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hypercorrection effect&lt;/strong&gt;: The finding that errors people make with high confidence are more likely to be corrected after feedback than errors made with low confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EEG&lt;/strong&gt;: Electroencephalography: recording the brain's electrical activity through electrodes on the scalp, precise enough in time to track responses to a single event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orienting task&lt;/strong&gt;: A task given during study that controls how people process each item, such as rating a word's pleasantness or checking it for a letter, used to study memory as a side effect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradient&lt;/strong&gt;: The direction and size of the steepest increase of a loss. A large gradient means the memory predicted this input badly and would change a lot to fit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Momentum&lt;/strong&gt;: A running, decaying sum of recent updates, added to the current one, so a strong push keeps having some effect for a few steps after it happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harness&lt;/strong&gt;: The software around a model that runs the agent loop: it sends prompts, executes tool calls, and can observe every call and its result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool call&lt;/strong&gt;: A structured request from the model to run an action, such as a shell command, a file read or a file edit, whose result is returned to the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit status&lt;/strong&gt;: The number a process returns when it ends. By convention 0 means success and anything else means failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AST&lt;/strong&gt;: Abstract syntax tree: the tree a parser builds from source code, with nodes for statements, expressions and names instead of raw characters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated program repair&lt;/strong&gt;: Tools that generate candidate patches for a failing program automatically, typically validated by running the test suite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peer dependency&lt;/strong&gt;: A package that declares it needs a particular version of another package installed alongside it. npm refuses to install when those requirements conflict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI (continuous integration)&lt;/strong&gt;: A service that builds and tests every change to a repository automatically. Travis CI was a widely used hosted CI service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flaky&lt;/strong&gt;: A test or build whose result can change between runs on the same code, because of timing, ordering, network or resource conditions rather than the code itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arc&lt;/strong&gt;: vectr's name for one fail, change, pass sequence: one or more failed runs of a command, and the passing run that resolves them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jaccard similarity&lt;/strong&gt;: Size of the intersection of two sets divided by the size of their union. Two identical sets score 1; two sets with nothing in common score 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surefire&lt;/strong&gt;: The Maven plugin that runs a project's unit tests during a build and prints the Tests run summary lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline&lt;/strong&gt;: In a shell, &lt;code&gt;a | b&lt;/code&gt; feeds a's output into b. The pipeline's exit status is b's unless the pipefail option is set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trajectory&lt;/strong&gt;: The full sequence of an agent's observations and actions while attempting one task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM&lt;/strong&gt;: Large language model: a neural network trained on large amounts of text to predict the next token, used here as the reasoning engine that drives an agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding&lt;/strong&gt;: A list of numbers representing a piece of text, placed so that texts with similar meaning land close together. Retrieval finds the nearest ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maven&lt;/strong&gt;: The standard build tool for Java projects. It compiles code and runs tests module by module according to each module's pom.xml.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module&lt;/strong&gt;: A sub-project of a multi-module build, with its own source tree and its own packaged output that other modules depend on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JUnit&lt;/strong&gt;: The standard unit-testing framework for Java. Its runners print summary lines such as "Tests run: 8, Failures: 0".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Kamin, L. J. "Predictability, surprise, attention, and conditioning." In B. A. Campbell and R. M. Church (eds.), &lt;em&gt;Punishment and Aversive Behavior&lt;/em&gt;, pp. 279-296. Appleton-Century-Crofts, 1969. Blocking, and its interpretation in terms of surprise.&lt;/li&gt;
&lt;li&gt;Moore, J. W., Schmajuk, N. A. &lt;a href="http://www.scholarpedia.org/article/Kamin_blocking" rel="noopener noreferrer"&gt;"Kamin blocking."&lt;/a&gt; &lt;em&gt;Scholarpedia&lt;/em&gt; 3(5):3542, 2008. The A+ then AB+ design, the control condition, and Kamin's 1968 and 1969 chapters.&lt;/li&gt;
&lt;li&gt;Rescorla, R. A., Wagner, A. R. "A theory of Pavlovian conditioning: Variations in the effectiveness of reinforcement and nonreinforcement." In A. H. Black and W. F. Prokasy (eds.), &lt;em&gt;Classical Conditioning II: Current Research and Theory&lt;/em&gt;, pp. 64-99. Appleton-Century-Crofts, 1972. The prediction-error learning rule.&lt;/li&gt;
&lt;li&gt;Rescorla, R. A. &lt;a href="http://www.scholarpedia.org/article/Rescorla-Wagner_model" rel="noopener noreferrer"&gt;"Rescorla-Wagner model."&lt;/a&gt; &lt;em&gt;Scholarpedia&lt;/em&gt; 3(3):2237, 2008. &lt;a href="https://doi.org/10.4249/scholarpedia.2237" rel="noopener noreferrer"&gt;doi:10.4249/scholarpedia.2237&lt;/a&gt;. The rule with every term defined, the account of blocking, the known shortcomings, and the equivalence to the Widrow-Hoff rule.&lt;/li&gt;
&lt;li&gt;Mackintosh, N. J. "A theory of attention: Variations in the associability of stimuli with reinforcement." &lt;em&gt;Psychological Review&lt;/em&gt; 82(4), 1975, pp. 276-298. &lt;a href="https://doi.org/10.1037/h0076778" rel="noopener noreferrer"&gt;doi:10.1037/h0076778&lt;/a&gt;. Pearce, J. M., Hall, G. "A model for Pavlovian learning: Variations in the effectiveness of conditioned but not of unconditioned stimuli." &lt;em&gt;Psychological Review&lt;/em&gt; 87(6), 1980, pp. 532-552. &lt;a href="https://doi.org/10.1037/0033-295X.87.6.532" rel="noopener noreferrer"&gt;doi:10.1037/0033-295X.87.6.532&lt;/a&gt;. The two attention-based alternatives to Rescorla-Wagner.&lt;/li&gt;
&lt;li&gt;Schultz, W., Dayan, P., Montague, P. R. "A Neural Substrate of Prediction and Reward." &lt;em&gt;Science&lt;/em&gt; 275(5306), 1997, pp. 1593-1599. &lt;a href="https://doi.org/10.1126/science.275.5306.1593" rel="noopener noreferrer"&gt;doi:10.1126/science.275.5306.1593&lt;/a&gt;. Dopamine responses to unpredicted, predicted and omitted reward, and the temporal-difference error term.&lt;/li&gt;
&lt;li&gt;Butterfield, B., Metcalfe, J. "Errors committed with high confidence are hypercorrected." &lt;em&gt;Journal of Experimental Psychology: Learning, Memory, and Cognition&lt;/em&gt; 27(6), 2001, pp. 1491-1494. &lt;a href="https://doi.org/10.1037/0278-7393.27.6.1491" rel="noopener noreferrer"&gt;doi:10.1037/0278-7393.27.6.1491&lt;/a&gt;. The original hypercorrection result.&lt;/li&gt;
&lt;li&gt;Butterfield, B., Metcalfe, J. "The correction of errors committed with high confidence." &lt;em&gt;Metacognition and Learning&lt;/em&gt; 1(1), 2006, pp. 69-84. &lt;a href="https://doi.org/10.1007/s11409-006-6894-z" rel="noopener noreferrer"&gt;doi:10.1007/s11409-006-6894-z&lt;/a&gt;. The tone-detection test of attentional capture during feedback, and the role of familiarity.&lt;/li&gt;
&lt;li&gt;Metcalfe, J. "Learning from Errors." &lt;em&gt;Annual Review of Psychology&lt;/em&gt; 68, 2017, pp. 465-489. &lt;a href="https://doi.org/10.1146/annurev-psych-010416-044022" rel="noopener noreferrer"&gt;doi:10.1146/annurev-psych-010416-044022&lt;/a&gt;. Review naming surprise and prior semantic knowledge as two non-exclusive factors behind hypercorrection.&lt;/li&gt;
&lt;li&gt;Hyde, T. S., Jenkins, J. J. "Recall for words as a function of semantic, graphic, and syntactic orienting tasks." &lt;em&gt;Journal of Verbal Learning and Verbal Behavior&lt;/em&gt; 12(5), 1973, pp. 471-480. &lt;a href="https://doi.org/10.1016/S0022-5371(73)80027-1" rel="noopener noreferrer"&gt;doi:10.1016/S0022-5371(73)80027-1&lt;/a&gt;. The five orienting tasks, with and without warning of a recall test.&lt;/li&gt;
&lt;li&gt;Tekin, E., Roediger, H. L. "Solving a Mystery in Levels-of-Processing Research: The Word-First Paradigm." &lt;em&gt;The American Journal of Psychology&lt;/em&gt; 134(2), 2021, pp. 129-141. &lt;a href="https://doi.org/10.5406/amerjpsyc.134.2.0129" rel="noopener noreferrer"&gt;doi:10.5406/amerjpsyc.134.2.0129&lt;/a&gt;. Summary of the finding that intentional learning without an orienting task yields retention similar to semantic orienting tasks, citing Hyde and Jenkins.&lt;/li&gt;
&lt;li&gt;Behrouz, A., Zhong, P., Mirrokni, V. &lt;a href="https://arxiv.org/abs/2501.00663" rel="noopener noreferrer"&gt;Titans: Learning to Memorize at Test Time&lt;/a&gt;. arXiv:2501.00663, 2025. Surprise defined as the gradient of the memory's loss, driving the memory update, with momentum and a forgetting gate.&lt;/li&gt;
&lt;li&gt;Koyuncu, A., Liu, K., Bissyandé, T. F., Kim, D., Klein, J., Monperrus, M., Le Traon, Y. "FixMiner: Mining relevant fix patterns for automated program repair." &lt;em&gt;Empirical Software Engineering&lt;/em&gt; 25(3), 2020, pp. 1980-2024. &lt;a href="https://doi.org/10.1007/s10664-019-09780-z" rel="noopener noreferrer"&gt;doi:10.1007/s10664-019-09780-z&lt;/a&gt;; &lt;a href="https://arxiv.org/abs/1810.01791" rel="noopener noreferrer"&gt;arXiv:1810.01791&lt;/a&gt;. Fix patterns mined from Rich Edit Scripts that carry the AST-level context of each change.&lt;/li&gt;
&lt;li&gt;Durieux, T., Le Goues, C., Hilton, M., Abreu, R. "Empirical Study of Restarted and Flaky Builds on Travis CI." &lt;em&gt;MSR 2020&lt;/em&gt;, pp. 254-264. &lt;a href="https://doi.org/10.1145/3379597.3387460" rel="noopener noreferrer"&gt;doi:10.1145/3379597.3387460&lt;/a&gt;; &lt;a href="https://arxiv.org/abs/2003.11772" rel="noopener noreferrer"&gt;arXiv:2003.11772&lt;/a&gt;. The 1.72% restart rate, 56,522 restarted builds, and 46.77% of failing or errored restarts passing on the same code.&lt;/li&gt;
&lt;li&gt;Apache Maven Surefire. &lt;a href="https://maven.apache.org/surefire/maven-surefire-plugin/examples/rerun-failing-tests.html" rel="noopener noreferrer"&gt;"Rerun Failing Tests."&lt;/a&gt; The &lt;code&gt;rerunFailingTestsCount&lt;/code&gt; option, and the rule that a test failing and then passing on a rerun is counted as a flake while the build succeeds.&lt;/li&gt;
&lt;li&gt;Zhao, A., Huang, D., Xu, Q., Lin, M., Liu, Y.-J., Huang, G. &lt;a href="https://arxiv.org/abs/2308.10144" rel="noopener noreferrer"&gt;ExpeL: LLM Agents Are Experiential Learners&lt;/a&gt;. AAAI 2024. arXiv:2308.10144. The experience pool, fail and success pairs for the same task, and insight extraction as a separate step.&lt;/li&gt;
&lt;li&gt;Wang, G., Xie, Y., Jiang, Y., Mandlekar, A., Xiao, C., Zhu, Y., Fan, L., Anandkumar, A. &lt;a href="https://arxiv.org/abs/2305.16291" rel="noopener noreferrer"&gt;Voyager: An Open-Ended Embodied Agent with Large Language Models&lt;/a&gt;. arXiv:2305.16291, 2023. Iterative prompting with execution feedback, self-verification, and the skill library indexed by description.&lt;/li&gt;
&lt;li&gt;Wang, Z. Z., Mao, J., Fried, D., Neubig, G. &lt;a href="https://arxiv.org/abs/2409.07429" rel="noopener noreferrer"&gt;Agent Workflow Memory&lt;/a&gt;. arXiv:2409.07429, 2024. Workflow induction, the online setting, and the workflow-as-action failure case in the flight-booking example.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/arc_replay_results.md" rel="noopener noreferrer"&gt;vectr, &lt;code&gt;benchmarks/arc_replay_results.md&lt;/code&gt;&lt;/a&gt;. The replay over 20 sessions (246 shell episodes, 9 edits, 0 arcs) and the hand audit: 0 error flags and 35 marker hits, all in runs ending in BUILD SUCCESS.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/vs_bash/tier1/README.md" rel="noopener noreferrer"&gt;vectr, &lt;code&gt;benchmarks/vs_bash/tier1/README.md&lt;/code&gt;&lt;/a&gt;. The bugfix-session readout: 25 Maven invocations, zero observed BUILD FAILUREs, single-module scope and piped or quieted output.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swapnanil/vectr/tree/main/results/vectr-vs-bash/camel/6b422df/t2" rel="noopener noreferrer"&gt;vectr, &lt;code&gt;results/vectr-vs-bash/camel/6b422df/t2/&lt;/code&gt;&lt;/a&gt;. The eight bugfix transcripts, including the reproduction run that went green on unfixed code and the model's reasoning at that point.&lt;/li&gt;
&lt;li&gt;vectr, &lt;a href="https://github.com/swapnanil/vectr/blob/main/app/arcs.py" rel="noopener noreferrer"&gt;&lt;code&gt;app/arcs.py&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/outcome.py" rel="noopener noreferrer"&gt;&lt;code&gt;agent/outcome.py&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/config.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;agent/config.yaml&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/markers.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;agent/markers.yaml&lt;/code&gt;&lt;/a&gt;. The detector, outcome classification with markers read before exit status, the similarity weights, band, window and flake thresholds, and the shipped marker table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/agents-forget-how-not-what/" rel="noopener noreferrer"&gt;Agents Forget How, Not What&lt;/a&gt;: Agent memory stores facts about code, but in recorded bugfix sessions the missing piece was how to run a test that could fail, and when that knowledge is absent nothing goes red.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/consolidation-is-the-missing-phase/" rel="noopener noreferrer"&gt;Consolidation Is the Missing Phase in Agent Memory&lt;/a&gt;: The offline phase where raw arcs should become lessons, and why the write path is the wrong place to integrate, abstract or select.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/search-is-memorys-write-path/" rel="noopener noreferrer"&gt;Search Is Memory's Write Path&lt;/a&gt;: Why the first search is the write for codebase knowledge, and why capture is the hard half when the conclusion lives in the model's reasoning rather than in the tool stream.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>testing</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Two Identical Chunks Are Not Always Duplicates</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:06:12 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/two-identical-chunks-are-not-always-duplicates-3hbl</link>
      <guid>https://dev.to/swapnanilsaha/two-identical-chunks-are-not-always-duplicates-3hbl</guid>
      <description>&lt;p&gt;&lt;em&gt;Three methods carrying one pasted docstring had to collapse into one search result. Fifty generated accessors must never collapse. A copy that was only renamed has exactly a template's shape, so whatever tells them apart is not in the text.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Correction, 27 October 2026&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An earlier version of this post described the three uv chunks as helpers for uv, Poetry and PDM that differ only in the tool each one runs. Those helpers are the stand-in in vectr's regression test, not uv's code. uv's real chunks are three abstract methods, and the identifier-masking guard of Section 04 does not fire on the two that collapse. The sections that relied on the stand-in now describe the real methods, and the claim that no text rule can separate a copy from a template instance is narrowed to copies that were only renamed. A reader's question about provenance sent me back to uv's source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A search that returns ten results and spends three of them on the same content has returned eight answers. The usual fix is textual: if two results are similar enough, show one of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/swapnanil/vectr" rel="noopener noreferrer"&gt;vectr&lt;/a&gt;, the MIT-licensed code-search engine I maintain, has two cases on record that its textual rules could not separate. In the source of a Python package manager, three methods in a benchmark suite carried one pasted docstring and nothing else. They took the top three of five results for a query about the lockfile type, and the type itself was not among the five. They had to collapse. In generated code, fifty accessors can share one docstring and differ only in an index. Each is a distinct definition, and collapsing them deletes forty-nine real answers.&lt;/p&gt;

&lt;p&gt;To the rules vectr ships, the two cases look the same: one shared docstring over chunks that score as near-identical. A finer rule that masks identifiers happens to tell the real uv methods apart, because their parameter lists differ, but a copy that was only renamed has exactly the shape of a template instance. So whether two chunks are duplicates is not a property of their text. It depends on facts outside the text: whether the chunks are copies or instances of a template, and what the person searching needs.&lt;/p&gt;

&lt;p&gt;A second finding sits under the first. On long, repetitive code, the similarity metric the rule relies on scored two near-identical bodies at 0.166 when the true figure is 0.995, and that error was the only thing stopping templated code from collapsing. Fixing the metric without re-deriving the threshold would have made deduplication far more aggressive in exactly the place it does the most damage.&lt;/p&gt;

&lt;p&gt;Ranking and deduplication are separate questions. Ranking decides which of two chunks goes first; deduplication decides whether they count as one answer. The ranking failures have their own write-ups: &lt;a href="https://swapnanilsaha.com/blog/embedding-dilution-code-search/" rel="noopener noreferrer"&gt;Embedding Dilution: Why Code Search Misses the Answer&lt;/a&gt; covers look-alikes outranking the target, and &lt;a href="https://swapnanilsaha.com/blog/deleting-ranking-heuristics-code-search/" rel="noopener noreferrer"&gt;I Deleted Every Ranking Heuristic From My Code Search&lt;/a&gt; covers why keyword rules cannot order code results. Deduplication starts where ranking stops, with a narrower question: given two results, are they the same answer?&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The Witness
&lt;/h2&gt;

&lt;h3&gt;
  
  
  01 · Why Code Search Results Need a Near-Duplicate Rule
&lt;/h3&gt;

&lt;p&gt;A code-search engine does not return files. It returns chunks, and the caller sees a short ranked list of them. Anything below the cut might as well not exist, so a slot spent twice on the same content is a slot taken from a different answer.&lt;/p&gt;

&lt;p&gt;The witness is a search for "the resolved lockfile object" against the source of uv. The answer is uv's &lt;code&gt;Lock&lt;/code&gt; struct, the type that models a lockfile. The top three of five results were instead three methods from uv's benchmark suite: &lt;code&gt;Suite.run&lt;/code&gt; in &lt;code&gt;tools.py&lt;/code&gt;, and &lt;code&gt;Suite.resolve_incremental&lt;/code&gt; and &lt;code&gt;Suite.resolve_noop&lt;/code&gt; side by side in &lt;code&gt;resolver.py&lt;/code&gt;. Each is an abstract method whose body is only its docstring, and all three open with the same sentence: &lt;em&gt;Resolve a modified lockfile using pip-tools.&lt;/em&gt; &lt;code&gt;run&lt;/code&gt; and &lt;code&gt;resolve_incremental&lt;/code&gt; carry the same four-line docstring word for word, in two different files; &lt;code&gt;resolve_noop&lt;/code&gt; drops "from a warm cache" from the first line, and drops the last sentence. The sentence mentions lockfiles, so each copy scored well, and because the copies barely differ, they scored about the same. Three of five slots went to one piece of content. The &lt;code&gt;Lock&lt;/code&gt; struct was missing for a second reason as well: vectr was not yet indexing Rust struct definitions as chunks of their own, which &lt;a href="https://github.com/swapnanil/vectr/commit/d81940c12649f7a8e386e3ecb2000b612c77662f" rel="noopener noreferrer"&gt;commit d81940c&lt;/a&gt; fixed.&lt;/p&gt;

&lt;p&gt;vectr's regression test for this case does not use these methods. It stands in for them with three small helpers, one each for uv, Poetry and PDM, that differ only in the function name and the tool each one runs. Section 04 shows why that difference matters.&lt;/p&gt;

&lt;p&gt;The fix was a second deduplication key. A key here is a value computed from each chunk; chunks that produce the same value become candidates for collapsing into one result. vectr already had an exact key, the chunk's normalized text, which collapses chunks that are byte-identical after normalization. The new key reads only a chunk's leading docstring: its first three lines, normalized, and at least 20 characters long so that a blank or one-word comment never counts. When candidates collapse, the best-ranked one stays visible as the representative. On the witness, the key folded &lt;code&gt;run&lt;/code&gt; and &lt;code&gt;resolve_incremental&lt;/code&gt; into one result; &lt;code&gt;resolve_noop&lt;/code&gt;, whose first line differs, kept its own slot.&lt;/p&gt;

&lt;p&gt;Keyed on the docstring alone, the rule merged a trait declaration with its own implementation, and it merged distinct constructors that happened to share a copy-pasted one-line summary. Sharing a docstring only shows that two chunks share a header. So the key was hardened with two gates, and a candidate collapses only when both pass:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structural.&lt;/strong&gt; If both chunks carry a recorded node type and the types differ, never collapse. A trait is never a near-duplicate of an impl, whatever the threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content.&lt;/strong&gt; The two whole chunks, normalized, must score at least 0.75 on the similarity ratio of Python's difflib.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The ratio behind the 0.75 gate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SequenceMatcher.ratio()&lt;/code&gt; is&lt;/p&gt;

&lt;p&gt;&lt;code&gt;R = 2M / T&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;where &lt;em&gt;T&lt;/em&gt; is the total number of characters in both strings and &lt;em&gt;M&lt;/em&gt; is the number of characters inside the matching blocks the algorithm finds. Identical strings score 1.0; strings with nothing in common score 0. Two strings of 100 characters that share 90 in matching blocks score 2 × 90 / 200 = 0.9.&lt;/p&gt;

&lt;p&gt;The blocks come from a procedure the Python documentation calls "a little fancier than" Ratcliff and Obershelp's 1980s "gestalt pattern matching": find the longest common contiguous run, then repeat on the pieces to its left and to its right. It is greedy, so it is not an edit distance, and &lt;em&gt;M&lt;/em&gt; can fall short of the longest common subsequence. The documentation also cautions that the ratio can change when the two arguments are swapped.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  The cost of comparing every pair
&lt;/h4&gt;

&lt;p&gt;Each candidate under a key was compared against every representative already kept under that key. When nothing collapses (fifty chunks that share a license header but differ in their bodies, say) the list of representatives grows with every candidate, and the loop makes on the order of n^2 comparisons. The 1.10.0 changelog records 3.1 seconds for 60 candidates sharing one key and 35.0 seconds for 200. Those two sizes come from the pipeline: 60 is the most candidates that reach this step after reranking, and 200 the ceiling when the reranker is off.&lt;/p&gt;

&lt;p&gt;Capping each candidate at the first three representatives brought the same cases to 0.30 and 1.07 seconds. The cap fails in only one direction. A candidate that would have matched a representative past the cap is never compared with it and stays as its own result, so the worst case is a wasted slot, never a hidden answer.&lt;/p&gt;

&lt;p&gt;Collapse also runs before the list is cut to the requested size, so slots freed by a collapse are refilled from the next distinct candidates. A dedup pass that ran after the cut would hand back fewer results than asked for and say nothing about it, which is the failure described in &lt;a href="https://swapnanilsaha.com/blog/filtering-after-top-k-silent-rag-bug/" rel="noopener noreferrer"&gt;Filtering After Top-k Is a Silent Bug in RAG&lt;/a&gt;. With both gates and the cap in place, the key shipped enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: The Metric
&lt;/h2&gt;

&lt;h3&gt;
  
  
  02 · How difflib's Autojunk Reads Repetitive Code Almost Backwards
&lt;/h3&gt;

&lt;p&gt;A later change added a second key for the mirror-image case: two chunks with different docstrings over near-verbatim bodies, which the docstring key cannot see at all. That key, &lt;code&gt;near_dup_body&lt;/code&gt;, compared whole chunks, so the differing docstrings sat inside the comparison and dragged the ratio under its threshold. Stripping the leading docstring before comparing moved the key's own test pair from 0.765 to 0.958.&lt;/p&gt;

&lt;p&gt;The same change found a larger defect underneath. Every ratio in this part of vectr was computed with &lt;code&gt;SequenceMatcher&lt;/code&gt;'s default setting, &lt;code&gt;autojunk=True&lt;/code&gt;. The Python 3.12 documentation states the rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If an item's duplicates (after the first one) account for more than 1% of the sequence and the sequence is at least 200 items long, this item is marked as 'popular' and is treated as junk for the purpose of sequence matching."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The current documentation presents junk handling as a heuristic for speed and for diffs that read better to people, whose ideal targets are "uninteresting or common items, such as blank lines or whitespace." That reading fits a line-by-line diff, where each item is a whole line and a line repeated dozens of times is usually blank or a lone brace. vectr was feeding it characters.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When a character becomes "popular"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a second sequence &lt;em&gt;b&lt;/em&gt; of length &lt;em&gt;n&lt;/em&gt; ≥ 200, CPython marks element &lt;em&gt;x&lt;/em&gt; popular when&lt;/p&gt;

&lt;p&gt;&lt;code&gt;count_b(x) &amp;gt; ⌊n / 100⌋ + 1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;At &lt;em&gt;n&lt;/em&gt; = 392 the bound is 4, so any character that appears five or more times is popular. In the varied body of Demo 01, only 234 characters long, the bound is 3 and eighteen characters qualify: the space, the newline, both parentheses and fourteen letters. Only the second argument's counts are consulted, so swapping the arguments can change which characters are popular, a second source of asymmetry on top of the greedy matching.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Why varied code survives autojunk and repetitive code does not
&lt;/h4&gt;

&lt;p&gt;Popular characters are not deleted from the strings. They are removed from the index that the matcher searches when it looks for the longest common run, so a popular character can never &lt;em&gt;start&lt;/em&gt; a match. It can still be absorbed into one: once a match has started on a rarer character, the algorithm extends it outward through any equal neighbours, popular or not.&lt;/p&gt;

&lt;p&gt;In varied code, rare characters are everywhere: a bracket, an underscore, a capital letter, a quote. Nearly every stretch of shared text contains one, a match starts there and grows across the popular characters around it, and the ratio comes out right. In repetitive code, a long stretch can consist of nothing but popular characters. Once the first difference ends a match, nothing in the rest of the text can start a new one, and every identical character after that point counts as unmatched.&lt;/p&gt;

&lt;p&gt;The committed test fixture behind the first row of the table below makes this concrete. It is a three-line Rust function followed by the comment &lt;code&gt;// implementation line&lt;/code&gt; repeated fifteen times, 392 characters in all, and its partner differs only in the first comment's last word (&lt;code&gt;note&lt;/code&gt; for &lt;code&gt;line&lt;/code&gt;). Twelve characters are popular, and they are exactly the characters of the repeated comment. The matcher finds one block, the 65 characters from the start of the function to the changed word. The 323 identical characters after that word, on both sides, go unmatched. The ratio is 2 × 65 / 784 = 0.166.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Body&lt;/th&gt;
&lt;th&gt;Length&lt;/th&gt;
&lt;th&gt;Default ratio&lt;/th&gt;
&lt;th&gt;autojunk=False&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Repetitive&lt;/td&gt;
&lt;td&gt;392 chars&lt;/td&gt;
&lt;td&gt;0.166&lt;/td&gt;
&lt;td&gt;0.995&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Varied&lt;/td&gt;
&lt;td&gt;251 chars&lt;/td&gt;
&lt;td&gt;0.996&lt;/td&gt;
&lt;td&gt;0.996&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repetitive&lt;/td&gt;
&lt;td&gt;150 chars&lt;/td&gt;
&lt;td&gt;0.993&lt;/td&gt;
&lt;td&gt;0.993&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distortion needs both conditions. Under 200 characters the heuristic never engages, and on varied code it changes nothing, which is why it went unnoticed. On long, repetitive code it reports two nearly identical bodies as barely similar at all.&lt;/p&gt;

&lt;h4&gt;
  
  
  The code autojunk distorts is the code dedup must not touch
&lt;/h4&gt;

&lt;p&gt;Long, repetitive code is what templates produce: field accessors, dispatch handlers, parameterized tests, generated client methods. Call it templated code. It is also the code a near-duplicate rule must never collapse, because every instance is a distinct symbol with its own name. The metric was wrong in exactly the region where being wrong kept the system safe. It scored template instances as dissimilar, so they never reached 0.75, so they never collapsed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The bug had been acting as a guard&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A defect that lines up with a hazard hides itself: every test of the hazard passes, for the wrong reason. vectr's tests include fifty Rust accessors that share a docstring and differ only in an index. I scored all 1,225 pairs the way the docstring key scores them, on the whole normalized chunk. With the default setting, every pair lands between 0.17 and 0.61, and none reaches 0.75. With &lt;code&gt;autojunk=False&lt;/code&gt;, every pair lands between 0.92 and 0.98, and every one would collapse. The same fifty functions stay separate or become one result depending on a flag nobody had set on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive · Demo 01: Watch autojunk discard the characters a match needs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pick a preset or edit either body. The strips show the second body character by character, once with the default and once with autojunk off: cyan was matched, red was not, and characters underlined in gold are "popular", so no match can start on them. Compare the repetitive preset with the varied one to see why length alone does no damage. (The interactive version runs on the original page.)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The matcher is a line-for-line port of CPython's difflib.SequenceMatcher, checked against Python on the presets and 300 random pairs. The first preset is the committed test fixture behind the 392-character row of the table; the other presets are shorter illustrations, so their lengths do not match the table's other rows. The demo compares the raw text in the boxes, without vectr's lowercasing and whitespace collapse, so the accessor preset reads slightly differently from the normalized figures above.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  03 · A Similarity Threshold Is Calibrated Against Its Metric's Errors
&lt;/h3&gt;

&lt;p&gt;The 0.75 threshold was chosen by measuring test pairs built to resemble real ones. Near-duplicates in the stand-in for the uv witness scored about 0.85 to 0.89 (the real pair, &lt;code&gt;run&lt;/code&gt; against &lt;code&gt;resolve_incremental&lt;/code&gt;, scores about 0.93), and two constructors sharing only a boilerplate doc line about 0.53, so 0.75 sat with margin on both sides. A trait and its impl sharing a doc comment scored about 0.86, inside the duplicate range; that pair is why the node-type gate exists, since no cutoff on this ratio could keep it apart. All of these numbers were measured with the default &lt;code&gt;autojunk&lt;/code&gt; in place.&lt;/p&gt;

&lt;p&gt;Correcting the metric and keeping 0.75 collapses the fifty accessors into a single result. That was measured, and the fix was split accordingly. &lt;code&gt;autojunk=False&lt;/code&gt; went in only where it changes no shipped behaviour: the &lt;code&gt;near_dup_body&lt;/code&gt; key, which ships disabled, and that key's guard. The enabled docstring key keeps the wrong parameter on purpose, with the finding written as a comment at the call site. The correct change pairs the corrected metric with a threshold re-measured on real corpora, in one commit, or it does not happen.&lt;/p&gt;

&lt;p&gt;A threshold is calibrated against a metric, including the metric's errors, so the two behave as one object. Fix the metric and the old number means something different; the pair has to be measured again together. The same thing happens whenever the model under a tuned constant changes. vectr shows a warning when no result looks like a confident match, triggered by a score floor that was tuned against one cross-encoder. The 1.12.0 changelog records that swapping in a different reranker left that constant stale.&lt;/p&gt;

&lt;p&gt;For anyone running difflib over source text, the check is mechanical. Find every &lt;code&gt;SequenceMatcher&lt;/code&gt; that leaves &lt;code&gt;autojunk&lt;/code&gt; at its default and can see a second argument of 200 or more elements. For each threshold downstream of one, find the pairs it was tuned on, re-score them with the flag off, and move the flag and the threshold in the same change.&lt;/p&gt;

&lt;p&gt;Budget for the flag, too, since the heuristic exists for speed. With popular characters left in the index, every space and every &lt;code&gt;e&lt;/code&gt; in the second string is a position the matcher walks. Timed under Python 3.11 on one machine, a single ratio on the 664-character normalized accessor pair takes 0.76 ms with the default and 17.6 ms with &lt;code&gt;autojunk=False&lt;/code&gt;; on a 2,685-character pair, 3.9 ms against 170 ms. The three-representative cap from Section 01 bounds how many comparisons run per candidate. The corrected metric still has to be costed against that budget before it ships on an enabled key, or compared over tokens or lines instead of characters, which shortens the sequences.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Agreement between two wrong computations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Part of why the defect survived: the tests computed their expected similarities with the same default call. Product and test agreed because both were wrong in the same way. A test that re-derives a value with the product's own library call can confirm that the call was made; it cannot catch an assumption the two share. The assertion that exposes this kind of defect states the outcome directly: fifty distinct accessors in, fifty results out. vectr now has one, recording the fifty-to-one collapse as the measured reason &lt;code&gt;near_dup_body&lt;/code&gt; stays off, and the similarity computations left in its tests use exactly the product's parameters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 3: The Guard
&lt;/h2&gt;

&lt;h3&gt;
  
  
  04 · Identifier Masking Finds Templated Code, and Copied Code Too
&lt;/h3&gt;

&lt;p&gt;If templated code is the hazard, the obvious guard is to recognise it. Two bodies that are the same code with different values in the slots, &lt;code&gt;accessor_0&lt;/code&gt; in one and &lt;code&gt;accessor_1&lt;/code&gt; in the other, are instances of a template and should never collapse. The guard went through three versions.&lt;/p&gt;

&lt;h4&gt;
  
  
  Digits, then edit operations, then masking
&lt;/h4&gt;

&lt;p&gt;The first version fired when most of the differing characters were digits. It caught &lt;code&gt;accessor_0&lt;/code&gt; against &lt;code&gt;accessor_1&lt;/code&gt; and missed &lt;code&gt;get_name&lt;/code&gt; against &lt;code&gt;get_email&lt;/code&gt;, which differ in letters and are just as distinct.&lt;/p&gt;

&lt;p&gt;The next version walked the diff's edit operations and required every changed span, on both sides, to consist only of letters, digits and underscores. A pure insertion or deletion has an empty side, so it failed the test. That tied the guard's verdict to how the aligner happened to line the strings up. &lt;code&gt;name&lt;/code&gt; against &lt;code&gt;phone&lt;/code&gt; is four characters against five, the aligner has to insert something, and the rule rejected the very shape it was built to catch. On one fixture it flagged &lt;code&gt;name&lt;/code&gt;/&lt;code&gt;email&lt;/code&gt; and missed &lt;code&gt;name&lt;/code&gt;/&lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;/&lt;code&gt;address&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;/&lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;/&lt;code&gt;address&lt;/code&gt; and &lt;code&gt;phone&lt;/code&gt;/&lt;code&gt;address&lt;/code&gt;. &lt;code&gt;name&lt;/code&gt;/&lt;code&gt;email&lt;/code&gt; passed only because the aligner matched their shared &lt;code&gt;a&lt;/code&gt; and paired the remaining letters as substitutions.&lt;/p&gt;

&lt;p&gt;The shipped version drops alignment entirely:&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="n"&gt;SLOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[A-Za-z_][A-Za-z0-9_]*|\d+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# identifier or bare number
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;same_shape_different_slots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;SLOT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;SLOT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every identifier-like token and every bare number becomes one placeholder, and the masked forms are compared. No alignment is involved, so the length of a token cannot change the answer. Token-based clone detectors such as CCFinder make a similar move, replacing identifiers before they compare so that renamed copies still match.&lt;/p&gt;

&lt;p&gt;The mask is coarse, and knowingly so. Keywords match the identifier pattern, so &lt;code&gt;return x&lt;/code&gt; and &lt;code&gt;yield x&lt;/code&gt; mask to the same thing, and so do two comments that differ by one word. Each of those errors labels a real difference as a template slot and keeps the two chunks apart, which for a guard whose only job is to prevent collapse is the cheap direction to be wrong in, for the same reason the comparison cap is.&lt;/p&gt;

&lt;h4&gt;
  
  
  Wired into the enabled key, the guard broke the stand-in, not the witness
&lt;/h4&gt;

&lt;p&gt;The masking guard works. It was then wired into the enabled docstring key, to protect that key the same way, and it broke the regression test for the uv witness. That test's helpers differ only in the function name and in the tool named inside the command. Masked, they are the same shape with different slots, exactly like the accessors. The guard shielded them from collapse, and the change was reverted.&lt;/p&gt;

&lt;p&gt;uv's real methods are a different shape. &lt;code&gt;Suite.run&lt;/code&gt; takes only &lt;code&gt;cwd&lt;/code&gt;, and &lt;code&gt;Suite.resolve_incremental&lt;/code&gt; also takes &lt;code&gt;requirements_file&lt;/code&gt;, so their masked forms differ. Run on the two real chunks, rebuilt from uv's source, the guard does not fire, and the pair collapses as before. The revert was decided on the stand-in alone; on the real witness, the guard would have cost nothing.&lt;/p&gt;

&lt;p&gt;The stand-in still shows something true. Copy a function and rename it, and the copy has exactly the shape of a template instance, so no rule that reads the characters of the two bodies can separate those two. Two tests are kept failing on purpose rather than deleted, marked xfail. Their fixtures differ only in a type name or only in a string literal, and they carry a shared docstring, so the enabled key collapses them on its own ratio gate and never consults the guard.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where the three keys stand
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;What it compares&lt;/th&gt;
&lt;th&gt;Ships&lt;/th&gt;
&lt;th&gt;Similarity gate&lt;/th&gt;
&lt;th&gt;Templated guard&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact&lt;/td&gt;
&lt;td&gt;Whole chunk, normalized&lt;/td&gt;
&lt;td&gt;Enabled&lt;/td&gt;
&lt;td&gt;Byte equality&lt;/td&gt;
&lt;td&gt;Not needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docstring&lt;/td&gt;
&lt;td&gt;First three docstring lines as the key, then the whole normalized chunk&lt;/td&gt;
&lt;td&gt;Enabled&lt;/td&gt;
&lt;td&gt;Ratio ≥ 0.75 with default &lt;code&gt;autojunk&lt;/code&gt;, kept on purpose; node types must match; at most 3 representatives compared&lt;/td&gt;
&lt;td&gt;Tried and reverted on the stand-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;near_dup_body&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Body with the leading docstring stripped&lt;/td&gt;
&lt;td&gt;Disabled&lt;/td&gt;
&lt;td&gt;Ratio ≥ 0.95 with &lt;code&gt;autojunk=False&lt;/code&gt;; node types must match&lt;/td&gt;
&lt;td&gt;Masking guard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive · Demo 02: Mask the identifiers, then ask what the reader needs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pick a pair. Both guards judge only the two bodies, after the same normalization vectr applies. The last row is the one no text rule computes: what someone searching loses if the pair collapses. The renamed copies and the accessors produce identical masked verdicts and need opposite actions; uv's real pair masks differently. (The interactive version runs on the original page.)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The renamed copies follow the stand-in in vectr's regression test for the uv witness; uv's real pair is the signature and docstring of &lt;code&gt;Suite.run&lt;/code&gt; and &lt;code&gt;Suite.resolve_incremental&lt;/code&gt;; the accessors are shortened from the fifty-accessor fixture. Edit a body and the last row switches to "you decide", because nothing in the two bodies can answer it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Could a cleverer text rule separate them?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Take the renamed copies and the accessors, and go through the candidates. Digits against letters: already tried, and &lt;code&gt;get_name&lt;/code&gt; must stay apart from &lt;code&gt;get_email&lt;/code&gt;. Symbol names: different in both pairs. Size of the difference: one renamed token in both. Position of the difference: the function name plus a value in the body, in both. An embedding model reads the same characters; nothing in its input marks one pair as copies and the other as instances, so any separation it produced would come from the particular words involved. A list of known tool names would split these two pairs and fail on the next repository. Anything that works has to consult something the two bodies do not contain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 4: The Relation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  05 · Duplicate Is a Relation Between Two Chunks and a Reader
&lt;/h3&gt;

&lt;p&gt;The negative result is more useful than the guard, because it says where the answer is not. Two older fields have faced the same question, and each answered it correctly for its own task.&lt;/p&gt;

&lt;h4&gt;
  
  
  Code clone types: how clone research classifies copies
&lt;/h4&gt;

&lt;p&gt;Clone research sorts clones by how their text differs. The standard taxonomy, as stated in Roy, Cordy and Koschke's 2009 comparison of clone detectors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type-1&lt;/td&gt;
&lt;td&gt;Identical code fragments except for variations in whitespace, layout and comments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type-2&lt;/td&gt;
&lt;td&gt;Syntactically identical fragments except for variations in identifiers, literals, types, whitespace, layout and comments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type-3&lt;/td&gt;
&lt;td&gt;Copied fragments with further modifications such as changed, added or removed statements, in addition to the Type-2 variations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type-4&lt;/td&gt;
&lt;td&gt;Two or more code fragments that perform the same computation but are implemented by different syntactic variants.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The renamed copies and the generated accessors are both Type-2 clones. Clone research also has a stricter test than vectr's mask. The same survey notes that most tools replace every identifier with one placeholder, as the masking guard does, while Baker's Dup encodes each identifier by position so that it finds only consistently renamed clones, where every occurrence of a name maps to the same replacement throughout. Both pairs pass that test as well: &lt;code&gt;poetry&lt;/code&gt; becomes &lt;code&gt;pdm&lt;/code&gt; everywhere it appears, and &lt;code&gt;3&lt;/code&gt; becomes &lt;code&gt;7&lt;/code&gt; everywhere it appears.&lt;/p&gt;

&lt;p&gt;For clone research, grouping both pairs is the right verdict, because its task is maintenance. In the survey's words, "if a bug is detected in a code fragment, all fragments similar to it should be checked for the same bug." A bug in one template instance is probably in all fifty, and a tool that groups them is doing its job.&lt;/p&gt;

&lt;p&gt;Clone research also found that the textual type does not settle whether a clone is a problem. Kapser and Godfrey grouped the clones they studied by the motivation behind them (forking, templating, customization) and argued that duplication is often a reasonable design choice. Motivation is a fact about how the code came to exist, and it is not in the characters.&lt;/p&gt;

&lt;h4&gt;
  
  
  Near-duplicate detection on the web: shingling and simhash
&lt;/h4&gt;

&lt;p&gt;Web search removes near-identical pages with shingling and similarity hashing. Broder's resemblance measure takes the shingle sets of two documents and computes r(A, B) = |S(A) ∩ S(B)| / |S(A) ∪ S(B)|. Charikar's simhash maps a document to a short fingerprint so that similar documents differ in few bits, and Manku, Jain and Das Sarma found 64-bit fingerprints with a tolerance of 3 differing bits workable for a repository of 8 billion pages. On the web the textual rule is mostly right: a reader after a page's content is served by any copy, and which copy to show is a separate question about provenance.&lt;/p&gt;

&lt;h4&gt;
  
  
  Code search is neither task
&lt;/h4&gt;

&lt;p&gt;The question "is B a duplicate of A?" is really "would this reader lose anything if shown A instead of B?" For the uv methods, the reader wanted the lockfile type, and any one of them was as useful as another, which is to say barely useful. For the accessors, a reader looking for &lt;code&gt;accessor_7&lt;/code&gt; loses everything if shown &lt;code&gt;accessor_3&lt;/code&gt;. The two pairs look alike to vectr's rules and get opposite answers, because the answer depends on the pair and on what the reader needs.&lt;/p&gt;

&lt;p&gt;The limit case needs no similarity score at all. Two classes that each define the same one-line &lt;code&gt;__repr__&lt;/code&gt;, printing the class name and an id through &lt;code&gt;self.__class__.__name__&lt;/code&gt;, produce two chunks whose text can be byte-identical. They are still two symbols, and a reader asking how the second class prints itself needs the second one. A key that reads only the chunk's characters has no way to see which class a chunk sits in.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Redundancy with and without the reader, in IR's own formulas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Information retrieval has both versions on record. Carbonell and Goldstein's maximal marginal relevance (MMR) picks each next result as&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MMR = argmax_{D_i ∈ R\S} [ λ·Sim_1(D_i, Q) - (1 - λ)·max_{D_j ∈ S} Sim_2(D_i, D_j) ]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;where &lt;em&gt;Q&lt;/em&gt; is the query, &lt;em&gt;R&lt;/em&gt; the retrieved list, &lt;em&gt;S&lt;/em&gt; the results already chosen, and λ trades relevance against novelty. The redundancy term Sim_2 compares two documents and never sees &lt;em&gt;Q&lt;/em&gt;. It is a text rule.&lt;/p&gt;

&lt;p&gt;Clarke and colleagues' α-nDCG measures redundancy through the reader instead. The information need is modelled as a set of nuggets, and the gain of the result at rank &lt;em&gt;k&lt;/em&gt; is&lt;/p&gt;

&lt;p&gt;&lt;code&gt;G[k] = Σ_i J(d_k, i) · (1 - α)^{r_{i,k-1}}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;where J(d_k, i) is 1 if result &lt;em&gt;k&lt;/em&gt; contains nugget &lt;em&gt;i&lt;/em&gt;, r_{i,k-1} counts the results above it that already contain that nugget, and α sets how steeply repeats lose value. A result is redundant exactly when the nuggets it carries are already covered. The one-token difference between &lt;code&gt;accessor_3&lt;/code&gt; and &lt;code&gt;accessor_7&lt;/code&gt; is almost nothing to Sim_2 and the entire nugget to someone looking for &lt;code&gt;accessor_7&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Philosophy has a name for the principle a text rule leans on. Leibniz's identity of indiscernibles, in the Stanford Encyclopedia's phrasing, is the thesis that "there cannot be two objects that differ only numerically": things alike in every property are one thing, not two. A text-similarity rule applies it with one property standing in for all of them, so two chunks that agree in text are treated as one while differing in their names, their callers and their reasons for existing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 5: The Signals
&lt;/h2&gt;

&lt;h3&gt;
  
  
  06 · Signals Outside the Text That Separate Copies From Instances
&lt;/h3&gt;

&lt;p&gt;What would separate the two classes are facts a text comparison never sees. None of them is wired into vectr's deduplication today. Each is a candidate with its own way of failing, and each needs measuring before it ships.&lt;/p&gt;

&lt;h4&gt;
  
  
  Copy or instance: the symbol graph
&lt;/h4&gt;

&lt;p&gt;A copy is the same content placed in several files by hand. An instance is one definition among many produced from a template. The symbol graph can ask the question the vectr commit names as missing: are the containing symbols distinct definitions, each referenced by its own name, or copies of one piece of content? Fifty accessors are fifty definitions, and code that reads a field calls the accessor for that field by name. uv's benchmark methods are definitions called by name too, by the suite's own dispatcher, and nothing in the library refers to them. A reference count alone would call both groups distinct. What marks the uv methods as clutter for a library question is where their references come from, which ties this signal to the file's role below.&lt;/p&gt;

&lt;p&gt;This signal breaks on generated API clients. Their public methods may have no callers inside the repository at all, because the callers are the client's users, so in the graph they look like code nothing uses. A reference count can only shift the odds.&lt;/p&gt;

&lt;h4&gt;
  
  
  The file's role and generated-code markers
&lt;/h4&gt;

&lt;p&gt;Scripts, fixtures and benchmark helpers usually live apart from library modules, in directories whose names say so. Generated files usually announce themselves. Go's toolchain documents a convention: a generated file carries a line matching &lt;code&gt;^// Code generated .* DO NOT EDIT\.$&lt;/code&gt; before its first non-comment text. vectr already recognises generated files by path (protobuf outputs, minified bundles, &lt;code&gt;generated/&lt;/code&gt; directories) and uses that to demote them in ranking. The dedup path does not consult it.&lt;/p&gt;

&lt;h4&gt;
  
  
  The history
&lt;/h4&gt;

&lt;p&gt;A copy has a moment in version control when content was duplicated from somewhere else. &lt;code&gt;git blame -C&lt;/code&gt; attributes lines to the file they were copied from (repeat the flag to search harder), and &lt;code&gt;git diff --find-copies&lt;/code&gt; does the same for whole files. Generated code has a generator instead, and usually a build step that runs it.&lt;/p&gt;

&lt;h4&gt;
  
  
  The query
&lt;/h4&gt;

&lt;p&gt;If the query names an identifier that distinguishes two chunks, they are not duplicates for that query, whatever their text. The check is cheap: take the tokens where the two chunks differ, split identifiers into their parts so that "accessor 7" in a query still matches &lt;code&gt;accessor_7&lt;/code&gt;, and look for any of them in the query. If none appears, showing one templated instance with a note that forty-nine similar ones exist may be exactly right. The price is that the decision now depends on the query, so it cannot be computed once at index time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Fold instead of delete
&lt;/h4&gt;

&lt;p&gt;Whatever the decision, it can be made reversible for the reader. Show one representative with "and N similar", keep the others one step away, and the result slots go to other answers without any claim that the others do not exist. A wrong fold costs the reader one click to expand; a wrong deletion costs the answer.&lt;/p&gt;

&lt;p&gt;vectr's result renderer does half of this today. A representative carries &lt;code&gt;(+N more identical)&lt;/code&gt;, but the collapsed chunks' locations are not kept, so they cannot be opened, and the label says "identical" for chunks the docstring key only judged near-identical. A real fold is the only one of these changes that needs no new signal, and it is the one I would ship first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Concept query: "how is the lockfile defined?"&lt;/th&gt;
&lt;th&gt;Instance query: "where is accessor_7 used?"&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keep all&lt;/td&gt;
&lt;td&gt;Three methods carrying one docstring take three of five slots.&lt;/td&gt;
&lt;td&gt;Fine: every accessor is still there.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collapse by text&lt;/td&gt;
&lt;td&gt;Partly: the key folds two of the three, since the third's first line differs, and one slot goes to another answer.&lt;/td&gt;
&lt;td&gt;Forty-nine accessors are gone; &lt;code&gt;accessor_7&lt;/code&gt; survives only if it happened to be the representative.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fold&lt;/td&gt;
&lt;td&gt;One row, "+1 similar", openable, and one slot goes to another answer.&lt;/td&gt;
&lt;td&gt;One accessor row, "+49 similar", openable; or no fold at all, because the query names &lt;code&gt;accessor_7&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Part 6: Predictions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  07 · What This Predicts, and How to Measure It
&lt;/h3&gt;

&lt;p&gt;The argument makes three claims that a measurement could break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No threshold works.&lt;/strong&gt; On a corpus with substantial generated or templated code, every text-similarity threshold will either leave copied-content clutter in the results or collapse distinct template instances. The measurement is a threshold sweep over a labelled set containing both classes. Half of that set exists: vectr's &lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/defc_autojunk/README.md" rel="noopener noreferrer"&gt;autojunk benchmark&lt;/a&gt; carries ten templated pairs in five shape families and reports, for each, which of the thresholds 0.75, 0.80, 0.85, 0.90, 0.95 and 0.99 would collapse it. The other half, labelled copies taken from real corpora, has not been built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One structural signal does.&lt;/strong&gt; Adding whether the two chunks define distinct symbols, and where their references come from, should separate the classes where text cannot. The uv methods are why the second half is needed: they are distinct definitions, called by name, but only from inside the benchmark suite. The same labelled set would measure it, and generated API clients, whose methods have no callers inside the repository, are where it should fail first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Folding wins on mixed traffic.&lt;/strong&gt; On a workload that mixes concept queries ("how is the lockfile defined?") with instance queries ("where is &lt;code&gt;accessor_7&lt;/code&gt; used?"), folding should beat both collapsing and keeping everything, since each of those loses one query type outright.&lt;/p&gt;

&lt;h3&gt;
  
  
  08 · The Line to Keep
&lt;/h3&gt;

&lt;p&gt;Three methods carrying one pasted docstring must collapse into one result, and fifty generated accessors must not. vectr's text rules cannot tell them apart, and against a copy that was only renamed, no rule that reads the text can. Whether two chunks are duplicates depends on what they are and who is asking, and that information lives outside the characters.&lt;/p&gt;

&lt;p&gt;Text similarity keeps a smaller job: proposing which chunks might be folded together. Deciding whether they should be needs the symbol graph, the file, the history or the query. Until one of those has been measured against labelled pairs, the defensible default for two look-alike chunks is a fold the reader can open, labelled "similar" rather than "identical".&lt;/p&gt;

&lt;h3&gt;
  
  
  09 · Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;vectr &lt;a href="https://github.com/swapnanil/vectr/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;, entries 1.12.0 ("Retrieval and ranking") and 1.10.0. The stripped-body ratios, the autojunk table, the fifty-to-one collapse, the reverted guard, the hardened docstring key and its latency measurements.&lt;/li&gt;
&lt;li&gt;vectr commit &lt;a href="https://github.com/swapnanil/vectr/commit/126e73dc877d371d58042a167df500e6d505e67f" rel="noopener noreferrer"&gt;126e73d&lt;/a&gt;, "Harden near-duplicate docstring dedup against false collapse". The two gates, the uv &lt;code&gt;Lock&lt;/code&gt; witness, and the margins measured for the 0.75 threshold.&lt;/li&gt;
&lt;li&gt;vectr commit &lt;a href="https://github.com/swapnanil/vectr/commit/a49949486a59dd2bc6f7db2f01803ab037b2606d" rel="noopener noreferrer"&gt;a499494&lt;/a&gt;, "Bound near-dup dedup comparison cost per doc_key". The per-key representative cap, its failure direction, and why 60 and 200 are the relevant candidate counts.&lt;/li&gt;
&lt;li&gt;vectr commit &lt;a href="https://github.com/swapnanil/vectr/commit/27bf9726aba2855a54bd6f283228bcc41354f033" rel="noopener noreferrer"&gt;27bf972&lt;/a&gt;, "compare stripped bodies for near-duplicate dedup, and find the autojunk defect". The discovery of the defect, the split fix, and the note on tests that share the product's assumption.&lt;/li&gt;
&lt;li&gt;vectr commit &lt;a href="https://github.com/swapnanil/vectr/commit/bc8b9f472e2660f608493e077bf10e896ea9bafd" rel="noopener noreferrer"&gt;bc8b9f4&lt;/a&gt;, "harness for the DEF-C autojunk threshold". The measured three-row table and the correction that the defect needs both length and repetition.&lt;/li&gt;
&lt;li&gt;vectr commit &lt;a href="https://github.com/swapnanil/vectr/commit/370b6f3067f228c5b73a867c15cb6453074b4aa7" rel="noopener noreferrer"&gt;370b6f3&lt;/a&gt;, "alignment-independent templated-body guard, and why it cannot fix DEF-C". The rename fixture, the masking rule, the reverted wiring, and the missing signal.&lt;/li&gt;
&lt;li&gt;vectr commit &lt;a href="https://github.com/swapnanil/vectr/commit/d81940c12649f7a8e386e3ecb2000b612c77662f" rel="noopener noreferrer"&gt;d81940c&lt;/a&gt;, "Chunk Rust struct/trait/enum, TS interface/type-alias/enum, Go type_declaration, Java interface/enum, Zig inline test blocks". Rust struct definitions indexed as chunks of their own.&lt;/li&gt;
&lt;li&gt;vectr source: &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/searcher.py" rel="noopener noreferrer"&gt;agent/searcher.py&lt;/a&gt; (the collapse loop and the call-site comment), &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/chunk_quality.py" rel="noopener noreferrer"&gt;agent/chunk_quality.py&lt;/a&gt; (the docstring key, the masking guard, generated-file detection), &lt;a href="https://github.com/swapnanil/vectr/blob/main/tests/test_defc_autojunk_harness.py" rel="noopener noreferrer"&gt;tests/test_defc_autojunk_harness.py&lt;/a&gt; (the 392-character fixture), &lt;a href="https://github.com/swapnanil/vectr/blob/main/tests/test_indexer_searcher.py" rel="noopener noreferrer"&gt;tests/test_indexer_searcher.py&lt;/a&gt; (the witness fixtures, the fifty-accessor fixture scored in Section 02, and the two xfail tests) and &lt;a href="https://github.com/swapnanil/vectr/blob/main/integrations/mcp_server/_dispatch.py" rel="noopener noreferrer"&gt;integrations/mcp_server/_dispatch.py&lt;/a&gt; (the "+N more identical" render).&lt;/li&gt;
&lt;li&gt;Python documentation, &lt;a href="https://docs.python.org/3.12/library/difflib.html" rel="noopener noreferrer"&gt;difflib, Python 3.12&lt;/a&gt; (the "Automatic junk heuristic" paragraph) and the &lt;a href="https://docs.python.org/3/library/difflib.html" rel="noopener noreferrer"&gt;current difflib page&lt;/a&gt; ("Junk heuristic"). The popularity rule, its framing as a heuristic for speed and readable diffs, the caution that ratio() can depend on argument order, and the Ratcliff and Obershelp lineage.&lt;/li&gt;
&lt;li&gt;Roy, C. K., Cordy, J. R., Koschke, R. "Comparison and evaluation of code clone detection techniques and tools: A qualitative approach." &lt;em&gt;Science of Computer Programming&lt;/em&gt; 74(7), 2009, pp. 470-495. &lt;a href="https://doi.org/10.1016/j.scico.2009.02.007" rel="noopener noreferrer"&gt;doi:10.1016/j.scico.2009.02.007&lt;/a&gt;. The Type-1 to Type-4 definitions, the maintenance rationale for grouping clones, and the contrast between single-placeholder identifier normalization and Baker's consistent-renaming scheme.&lt;/li&gt;
&lt;li&gt;Baker, B. S. "On finding duplication and near-duplication in large software systems." WCRE 1995, pp. 86-95. &lt;a href="https://doi.org/10.1109/WCRE.1995.514697" rel="noopener noreferrer"&gt;doi:10.1109/WCRE.1995.514697&lt;/a&gt;. Dup, the token-based detector that finds parameterized, consistently renamed clones.&lt;/li&gt;
&lt;li&gt;Kapser, C., Godfrey, M. W. "'Cloning Considered Harmful' Considered Harmful." WCRE 2006, pp. 19-28. &lt;a href="https://doi.org/10.1109/WCRE.2006.1" rel="noopener noreferrer"&gt;doi:10.1109/WCRE.2006.1&lt;/a&gt;. Extended as "'Cloning considered harmful' considered harmful: patterns of cloning in software," &lt;em&gt;Empirical Software Engineering&lt;/em&gt; 13(6), 2008, pp. 645-692, &lt;a href="https://doi.org/10.1007/s10664-008-9076-6" rel="noopener noreferrer"&gt;doi:10.1007/s10664-008-9076-6&lt;/a&gt;. Clone patterns grouped by motivation: forking, templating, customization.&lt;/li&gt;
&lt;li&gt;Broder, A. Z. "On the resemblance and containment of documents." Compression and Complexity of Sequences 1997, pp. 21-29. &lt;a href="https://doi.org/10.1109/SEQUEN.1997.666900" rel="noopener noreferrer"&gt;doi:10.1109/SEQUEN.1997.666900&lt;/a&gt;. Shingling and the resemblance measure.&lt;/li&gt;
&lt;li&gt;Charikar, M. S. "Similarity estimation techniques from rounding algorithms." STOC 2002, pp. 380-388. &lt;a href="https://doi.org/10.1145/509907.509965" rel="noopener noreferrer"&gt;doi:10.1145/509907.509965&lt;/a&gt;. The locality-sensitive fingerprint known as simhash.&lt;/li&gt;
&lt;li&gt;Manku, G. S., Jain, A., Das Sarma, A. "Detecting near-duplicates for web crawling." WWW 2007, pp. 141-150. &lt;a href="https://doi.org/10.1145/1242572.1242592" rel="noopener noreferrer"&gt;doi:10.1145/1242572.1242592&lt;/a&gt;. Simhash at the scale of 8 billion pages, with 64-bit fingerprints and k = 3.&lt;/li&gt;
&lt;li&gt;Carbonell, J., Goldstein, J. "The use of MMR, diversity-based reranking for reordering documents and producing summaries." SIGIR 1998, pp. 335-336. &lt;a href="https://doi.org/10.1145/290941.291025" rel="noopener noreferrer"&gt;doi:10.1145/290941.291025&lt;/a&gt;. Maximal marginal relevance and its query-free redundancy term.&lt;/li&gt;
&lt;li&gt;Clarke, C. L. A., Kolla, M., Cormack, G. V., Vechtomova, O., Ashkan, A., Büttcher, S., MacKinnon, I. "Novelty and diversity in information retrieval evaluation." SIGIR 2008, pp. 659-666. &lt;a href="https://doi.org/10.1145/1390334.1390446" rel="noopener noreferrer"&gt;doi:10.1145/1390334.1390446&lt;/a&gt;. Information nuggets and the α-nDCG gain vector.&lt;/li&gt;
&lt;li&gt;Rodriguez-Pereyra, G. &lt;a href="https://plato.stanford.edu/entries/identity-indiscernible/" rel="noopener noreferrer"&gt;"The Identity of Indiscernibles."&lt;/a&gt; &lt;em&gt;Stanford Encyclopedia of Philosophy&lt;/em&gt;. The statement of the principle quoted in Section 05.&lt;/li&gt;
&lt;li&gt;The Go command documentation, &lt;a href="https://pkg.go.dev/cmd/go#hdr-Generate_Go_files_by_processing_source" rel="noopener noreferrer"&gt;"Generate Go files by processing source"&lt;/a&gt;. The generated-code marker convention.&lt;/li&gt;
&lt;li&gt;Git documentation, &lt;a href="https://git-scm.com/docs/git-blame" rel="noopener noreferrer"&gt;git-blame&lt;/a&gt; (the &lt;code&gt;-C&lt;/code&gt; option) and &lt;a href="https://git-scm.com/docs/git-diff" rel="noopener noreferrer"&gt;git-diff&lt;/a&gt; (&lt;code&gt;--find-copies&lt;/code&gt;). Line-level and file-level copy detection in version control.&lt;/li&gt;
&lt;li&gt;uv at commit &lt;code&gt;7685f53&lt;/code&gt;: &lt;a href="https://github.com/astral-sh/uv/blob/7685f5331bb62fe47fb03a73b0d6422ca211cce9/scripts/benchmark/src/benchmark/tools.py#L60-L67" rel="noopener noreferrer"&gt;scripts/benchmark/src/benchmark/tools.py, lines 60-67&lt;/a&gt; and &lt;a href="https://github.com/astral-sh/uv/blob/7685f5331bb62fe47fb03a73b0d6422ca211cce9/scripts/benchmark/src/benchmark/resolver.py#L126-L143" rel="noopener noreferrer"&gt;resolver.py, lines 126-143&lt;/a&gt;. uv's benchmark suite, which times uv against other Python packaging tools: the three abstract methods of the witness.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/embedding-dilution-code-search/" rel="noopener noreferrer"&gt;Embedding Dilution: Why Code Search Misses the Answer&lt;/a&gt;: Why a chunk whose docstring matches the query can still rank below 200 look-alikes.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/deleting-ranking-heuristics-code-search/" rel="noopener noreferrer"&gt;I Deleted Every Ranking Heuristic From My Code Search&lt;/a&gt;: What replaced the heuristics once every query-side branch was gone.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/filtering-after-top-k-silent-rag-bug/" rel="noopener noreferrer"&gt;Filtering After Top-k Is a Silent Bug in RAG&lt;/a&gt;: Fetch the nearest k, then filter: the result is short and nothing says so.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>search</category>
      <category>python</category>
      <category>algorithms</category>
      <category>rag</category>
    </item>
    <item>
      <title>Filtering After Top-k Is a Silent Bug in RAG</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Mon, 12 Oct 2026 16:53:55 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/filtering-after-top-k-is-a-silent-bug-in-rag-2905</link>
      <guid>https://dev.to/swapnanilsaha/filtering-after-top-k-is-a-silent-bug-in-rag-2905</guid>
      <description>&lt;p&gt;A filtered vector query, the kind that sits in the retrieval step of a RAG pipeline or behind an agent's memory lookup, is commonly written as two lines. Ask the vector index for the nearest k items, then drop the ones that fail a condition: wrong workspace, wrong type, expired, deleted, not visible to this user.&lt;/p&gt;

&lt;p&gt;Those two lines do not compute what they appear to compute. The index answered "which k items are nearest overall?" The caller wanted "which k items are nearest among the ones that pass?" Composing the first with a filter does not produce the second.&lt;/p&gt;

&lt;p&gt;The gap has two properties that make it worse than an ordinary bug. &lt;strong&gt;It returns a well-formed, correctly ordered, too-short result&lt;/strong&gt;, which looks exactly like a correct answer from a store that happens to hold little. And &lt;strong&gt;it is worst precisely when the filter matters most&lt;/strong&gt;: when the items you want are rare, or when the items you do not want are the ones most similar to the query.&lt;/p&gt;

&lt;p&gt;Every example below comes from one agent-memory store, the open-source &lt;a href="https://github.com/swapnanil/vectr" rel="noopener noreferrer"&gt;vectr&lt;/a&gt; daemon, where the same bug turned up in at least four separate code paths between late July and late August 2026. Each was found and fixed on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The Arithmetic
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What a Post-Filter Computes in Vector Search
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A recruiter is asked for the ten CVs that best match a job description and hands over ten. The hiring manager strikes everyone without the right to work in the country and interviews whoever is left. If the ten best matches all came from abroad, nobody is left, and the manager concludes that no local applicant fits. Applicants eleven to fifty were never read. The conclusion is about the shortlist, not the applicant pool, and nothing on the empty interview schedule says which.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a post-filter: a yes-or-no condition applied after the ranking has already been cut to k. Put numbers on it. Call s the pass rate near this query, meaning the chance that a candidate from the top of its ranking passes the filter. Fetch k candidates and you expect k × s survivors, so returning n results takes, on average, k ≥ n / s.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The post-filter, exactly&lt;/strong&gt;&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;survivors(k) = | { x in top_k(q) : passes(x) } |
E[survivors(k)] = k · s
returns the true filtered top-n  ⟺  k ≥ r_n
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;q is the query. top_k(q) is the set of k items nearest to q in the whole store. s is the pass rate among q's near neighbours. r_n is the rank, in the unfiltered ordering, of the n-th nearest item that passes. A fetch of depth k returns the true filtered answer if and only if it reaches r_n. Short of that depth it returns the first few items of the right answer, in the right order, and then stops.&lt;/p&gt;

&lt;p&gt;When the filter has nothing to do with distance, s is just σ (sigma), the share of the whole store that passes. The survivor count is then approximately binomial, P(no survivors) = (1 - σ)^k, and r_n follows a negative binomial distribution with mean n / σ.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read the third line again, because it holds the most useful fact about the bug. &lt;strong&gt;A post-filter over an exact nearest-neighbour ranking never returns a wrong item.&lt;/strong&gt; Everything it returns belongs in the answer, at the right position. Its only error is omission, the tail that fell outside the fetch, and omission leaves nothing behind to inspect. (An approximate index misses a few true neighbours of its own; the post-filter's omissions come on top.) This is also why the pattern gets through code review. Read as an access-control check, a post-filter is safe: it cannot leak an item the user is not allowed to see. The damage is all on the recall side, where a reviewer checking for leaks is not looking.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why s is not the share of the store
&lt;/h4&gt;

&lt;p&gt;The trap is in what s means. It is not σ, the share of the whole store that passes. It is the share of &lt;em&gt;this query's&lt;/em&gt; nearest neighbours that pass, and once the filter is correlated with similarity the two can be far apart.&lt;/p&gt;

&lt;p&gt;The damaging direction is a filter that removes the closest items. In a store shared across projects, the items most similar to a query about project A are likely to be items on the same topic from project B, because topic drives similarity and projects share topics. A workspace filter removes exactly those. Project A can own half the store and still own none of a query's ten nearest neighbours. The global share does not predict this; it depends on how the filter lines up with the embedding space, and it changes from query to query.&lt;/p&gt;

&lt;p&gt;Rarity hurts even with no correlation at all. In one live store of 565 notes, 2 were in the revoked state: σ = 0.35%. Fetch 3 candidates and the expected number of revoked survivors is 3 × 0.0035, about 0.01. Under the independence model, 100 candidates contain a revoked note about 30% of the time, and a 95% chance takes about 845.&lt;/p&gt;

&lt;p&gt;Correlation can also run the helpful way, when the wanted items are the ones nearest the query. That case comes up with the revoked notes below, and it is the reason a fixed depth sometimes works far better than σ predicts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive demo: Fetch k, Then Filter&lt;/strong&gt; (runs on the &lt;a href="https://swapnanilsaha.com/blog/filtering-after-top-k-silent-rag-bug/" rel="noopener noreferrer"&gt;original page&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;See that a post-filter is never wrong, only short, and find the fetch depth each kind of filter needs. Each dot is an item in a small seeded store and the gold cross is the query. Everything inside the dashed circle is in the top-k. Switch to the second scenario, where the closest items belong to another workspace, and find the smallest k that returns a full answer; then try the rare class and compare "the store ran out" with "the search ran out".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;48 items in two dimensions, seeded so the layout reproduces. Real embeddings have hundreds of dimensions, but ranking by distance and then filtering behaves the same way. The "caller sees" lines are everything a real caller gets; the "ground truth" lines are what only the demo knows.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Over-Fetch Multipliers Are a Bet on the Pass Rate
&lt;/h3&gt;

&lt;p&gt;The usual mitigation is to over-fetch by a constant: ask for &lt;code&gt;limit * 3&lt;/code&gt; and filter down to &lt;code&gt;limit&lt;/code&gt;. The constant encodes an assumed s. Three times the limit assumes that a third of the neighbours pass. For some queries and filters that is right, for others it is silently wrong, and the code has no way to tell which case it is in.&lt;/p&gt;

&lt;p&gt;Underneath that sits a quieter problem. Even when the assumed s is exactly right, n / s is the &lt;em&gt;average&lt;/em&gt; depth needed, and the actual depth scatters around it. With s = 1/3, n = 5 and k = 15, the chance that at least five survive is 60%. Getting to 95% takes k = 25: five times the limit, not three. Rarer classes need more headroom still. At s = 1/7, a 95% chance of five survivors takes 62 candidates, a multiplier above twelve.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A multiplier sized to the average pass rate fills the answer somewhere between 55 and 70% of the time for the values used here. Reliability comes from headroom above n / s, and no single constant supplies the right headroom for a filter whose pass rate changes with the query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive demo: A Multiplier Is a Bet on the Pass Rate&lt;/strong&gt; (runs on the &lt;a href="https://swapnanilsaha.com/blog/filtering-after-top-k-silent-rag-bug/" rel="noopener noreferrer"&gt;original page&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Learn how much headroom a fixed multiplier really buys. Set the pass rate among a query's neighbours, the number of results you need, and the multiplier, and read off how often the fetch comes back full. The presets are the pass rates that appear in the four instances below. The model assumes the filter is independent of distance. A filter that removes the closest items does worse than the numbers shown; one whose wanted items sit close to the query does better.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;P(at least n survive) is the binomial tail with k = multiplier × n tries at pass rate s. "Multiplier for 95%" searches upward for the smallest multiplier whose tail reaches 0.95.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Four Instances in One Memory Store
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Instance 1: Related Notes Starved to an Empty List
&lt;/h3&gt;

&lt;p&gt;The store keeps an agent's notes in a relational table and their embeddings in a separate vector collection. Each note belongs to a workspace, has a kind (a standing rule, a task, a caveat, and so on), may have expired, and has a lifecycle state.&lt;/p&gt;

&lt;p&gt;When a note is written, the store looks up the most similar existing notes and shows them to the writer, so it can see what the new note might duplicate or contradict. The vector collection was &lt;strong&gt;global across every workspace the daemon served&lt;/strong&gt;, and the embeddings were written with their ids and nothing else, so the similarity query had nothing to filter on. The workspace check, the expiry check and the lifecycle check all ran in SQL and Python &lt;em&gt;after&lt;/em&gt; the vector search.&lt;/p&gt;

&lt;p&gt;The query asked for &lt;code&gt;limit + 1&lt;/code&gt; neighbours, budgeting for exactly one exclusion: the note itself. When the nearest neighbours belonged to another workspace or had already been revoked, they used up the whole budget and the filters removed every one of them. The function returned an empty list &lt;strong&gt;even when close, active, same-workspace notes existed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;agent/working_context_store/_related.py, commit 77252a6&lt;/em&gt;&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="c1"&gt;# before: room to exclude the note itself, and nothing else
&lt;/span&gt;&lt;span class="n"&gt;n_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# after
&lt;/span&gt;&lt;span class="n"&gt;n_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;col_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That repair is a fixed multiplier too, and so a bet on s. It holds while the querying workspace owns most of the neighbourhood. Take a daemon serving ten projects of similar size with no topic overlap, so s is about a tenth, and the shipped limit of 3. The fetch is 9 candidates, and the first of them is the new note itself, which the lookup skips. The remaining 8 yield 0.8 survivors on average and a full list of three about 4% of the time. Topic overlap between the projects makes it worse, since overlap is exactly the correlation that puts another project's notes at the front.&lt;/p&gt;

&lt;h3&gt;
  
  
  Instance 2: A Rare Class Starved by the Render Limit
&lt;/h3&gt;

&lt;p&gt;The same write-time lookup has a second path: find revoked notes similar to the new one, so an agent re-deriving a belief that was already proven wrong is warned before it writes the belief down again. That path copied the first path's pool formula, which tied search depth to how many results would be &lt;em&gt;rendered&lt;/em&gt;. With one revoked result rendered, the pool was 3 candidates.&lt;/p&gt;

&lt;p&gt;Measured on a live 565-note store, revoked notes were &lt;strong&gt;2 of 565, 0.35%&lt;/strong&gt;, against roughly 99% active. A pool of 3 has no room for a crowd. Three closer active notes on the same topic always push the one relevant revoked note out before the lifecycle filter runs, and in the normal write path two are enough, because the new note's own vector is already in the collection and takes the first slot. The two paths had target classes whose prevalence differed by more than two orders of magnitude, and they shared one formula.&lt;/p&gt;

&lt;p&gt;Commit 8f51803 decoupled depth from render count with a flat floor of 100 candidates on the revoked path. The measured cost, on a synthetic 50,000-vector collection: about 0.59 ms mean for 3 results against about 0.94 ms for 100. Under a millisecond either way. The bug had been saving 0.35 ms per write.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why the floor is flat
&lt;/h4&gt;

&lt;p&gt;The floor is 100 whatever the store size, and prevalence did not set it. Under the independence model, 100 candidates find a randomly placed revoked note less than a third of the time, so if prevalence were the whole story, 100 would be far too few. Here the correlation runs the helpful way. The revoked note worth surfacing is a near-duplicate of the note being written, so it ranks high. What pushes it out of the pool is the handful of active notes on the same subject that rank higher still, and that handful is bounded by how much anyone writes about one topic, not by how large the store grows. Anything further down would not be shown anyway: the lookup drops candidates below a cosine similarity of 0.75 by default.&lt;/p&gt;

&lt;p&gt;Prevalence, then, explains why the active path's formula was wrong for this path. The depth that is right comes from a different number, the count of non-passing notes that can crowd in above the answer.&lt;/p&gt;

&lt;p&gt;What such a warning should say once it is found is a separate question, taken up in &lt;a href="https://swapnanilsaha.com/blog/should-a-correction-repeat-the-mistake/" rel="noopener noreferrer"&gt;Should a Correction Repeat the Mistake?&lt;/a&gt;. The wording matters only if retrieval reaches the revoked record at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Instance 3: One Arm of an OR Query Flooding a Shared LIMIT
&lt;/h3&gt;

&lt;p&gt;Path-based recall answers "which notes concern the file about to be edited?" A note can match in three ways: its text mentions the path, it is explicitly anchored to the file, or it declares a trigger glob that matches it. All three were combined with OR into &lt;strong&gt;one SQL query with one LIMIT&lt;/strong&gt;, and Python did the exact matching afterwards.&lt;/p&gt;

&lt;p&gt;SQL could scope the first two arms to the file with LIKE patterns on the file name. It could not scope the third. A glob such as &lt;code&gt;**/*.py&lt;/code&gt; has no LIKE equivalent, so the SQL step matched &lt;em&gt;every note that declares any path trigger at all&lt;/em&gt;, anywhere in the workspace, and left the glob to Python. A workspace with enough such notes filled the shared LIMIT and pushed genuine file-scoped matches off the end before the narrowing ran. In a stress run of the test harness with a broad glob, &lt;strong&gt;20 of 20 test windows lost their genuine anchored match&lt;/strong&gt;. After the fix, 0 of 20 did.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;recall_for_path, commit 24fba5f (simplified)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- before: three arms, one pool, one LIMIT&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;valid_until&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;anchors&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;triggers&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%"path"%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- after: the arm SQL cannot scope gets its own LIMIT&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;valid_until&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;anchors&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;valid_until&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;triggers&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%"path"%'&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- union, dedupe by note_id, then narrow in Python&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit 24fba5f split the query in two: one bounded query for the arms SQL can scope to the file, one bounded query for the trigger arm, unioned and deduplicated by note id, and only then narrowed. However many notes declare triggers, they can no longer take a file-scoped match's place in the pool.&lt;/p&gt;

&lt;p&gt;A second defect sat behind the first. Once the pool was right, genuine matches could still exceed the limit, and the final cut to &lt;code&gt;limit&lt;/code&gt; was ordered by recency alone, so a newer weak match could displace an older strong one. Matches are now ranked by relation strength first (a declared anchor, then a matching trigger, then a mention in the text) and by recency within each tier. Fixing the pool moved the truncation one stage downstream, and that second cut needed its own ordering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Instance 4: A Kind Filter and a Sort the Pool Could Not Reach
&lt;/h3&gt;

&lt;p&gt;Recall can be filtered by note kind, one of seven. The kind filter ran after a fixed &lt;code&gt;limit * 3&lt;/code&gt; semantic fetch, and a selective kind returned fewer than &lt;code&gt;limit&lt;/code&gt; notes &lt;strong&gt;with no signal that the shortfall came from the pool&lt;/strong&gt; rather than from an absence of matching notes.&lt;/p&gt;

&lt;p&gt;Three days before the kind filter was fixed, the codebase had pulled the over-fetch rule into a single helper whose docstring states the invariant for every query against the collection: size the fetch through the helper, never with a raw render limit. The kind filter went through the helper and still under-filled. The helper's rule is a multiplier of 3, which assumes a third of the neighbours pass, and a filter that keeps one kind in seven sits well outside that assumption. A shared helper makes every call site pay the same bet; it cannot know that one caller's filter is seven times narrower than another's.&lt;/p&gt;

&lt;p&gt;Now the pool widens to &lt;code&gt;limit * 10&lt;/code&gt; when a kind is set, a figure the code comment calls "a guess, not a measurement". Under independence, with a limit of 5, that returns a full five notes of a one-in-seven kind about 86% of the time. The rest is covered by a backstop: a plain query on the notes table, with no vector search involved, that pulls the newest notes of the requested kind and appends any the pool missed after the ranked results. It guarantees the count, not the ranking. Those appended notes carry no similarity score and sit after every note the semantic pool did rank, however relevant they are.&lt;/p&gt;

&lt;h4&gt;
  
  
  Sorting a pool cannot reach what the pool missed
&lt;/h4&gt;

&lt;p&gt;A close relative sits in the same function. Recall can also be sorted by recency, priority or chronology instead of relevance, and those sorts were applied to the semantic candidate pool. The most recent note, or the highest-priority one, may not be semantically close to the query at all, so it never entered the pool, and &lt;strong&gt;no re-sorting of a pool can surface an item that is not in it.&lt;/strong&gt; Sorting a similarity pool by recency answers "the most recent of the similar items", which is rarely what "sort by recency" was meant to mean.&lt;/p&gt;

&lt;p&gt;Explicit sorts now re-query the store under the requested ordering, so membership follows the requested key rather than query similarity. Rows fetched that way are not dropped for low similarity either: they were never scored, and a similarity floor means nothing for a row with no score. The regression test states the case exactly: seven on-topic decisions and one decision about an unrelated subsystem, dated oldest of all, with an embedding orthogonal to the query so it cannot enter the pool. A chronological recall must return it first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/blog/relevance-is-the-wrong-sort-order/" rel="noopener noreferrer"&gt;Relevance Is the Wrong Sort Order for Agent Memory&lt;/a&gt; takes up which ordering is right for memory in the first place. The mechanical constraint holds whichever ordering wins: it has to be applied to a candidate set chosen by that ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: Why Nothing Noticed
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A Short Result Is a Claim About the Data
&lt;/h3&gt;

&lt;p&gt;Each instance survived because its output was plausible. An empty related-notes list reads as "nothing related exists". Two notes where five were asked for reads as "only two match". When the revoked-note warning never appears, the natural reading is that the belief was never revoked. In every case the wrong answer is a &lt;em&gt;valid&lt;/em&gt; answer for a slightly different store, and neither the caller nor a test that checks well-formedness can tell it apart from the right one.&lt;/p&gt;

&lt;p&gt;That is the general danger of truncating and then filtering: &lt;strong&gt;it converts a search error into an apparent fact about the data.&lt;/strong&gt; A retrieval layer that returns fewer items than requested is making an implicit claim, "there are no more", and a post-filter makes that claim without having checked it.&lt;/p&gt;

&lt;p&gt;For agents the cost compounds. A person scanning search results may notice that something they expected is missing and search again. An agent that recalls memory and gets a short list has no signal that would prompt a second, deeper query, so it proceeds as if the list were complete. The missing note is absent from every decision downstream of that answer, and nothing in the transcript marks where it would have mattered.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Offline evaluation can share the hole. recall@k is only as good as its ground truth, and a ground truth built by running the same filtered query inherits the same omissions. Build it by filtering the whole store first and ranking the survivors exactly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 4: Fixes That Hold
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fixing Post-Filtering in Application Code
&lt;/h3&gt;

&lt;p&gt;The list runs from weakest to strongest. The weaker fixes are worth knowing because the strongest one depends on the engine, and on the data being in the engine at all.&lt;/p&gt;

&lt;h4&gt;
  
  
  Report the underfill
&lt;/h4&gt;

&lt;p&gt;When fewer than n results survive and the fetch stopped before the store did, say so: "returned 2 of 5 requested; candidate pool of 15 exhausted". The answer is no better, but a silent claim has become a visible uncertainty that a caller, or an agent, can act on. Telling the two cases apart is cheap if you are careful about one detail. The store ran out if the fetch covered every item, or if the last candidate fetched already falls below the similarity floor, since nothing further down could have been shown. A fetch that came back with fewer rows than it asked for is &lt;em&gt;not&lt;/em&gt; proof on its own: some indexes cap what one scan can return. pgvector's HNSW scan, for one, returns at most hnsw.ef_search rows (40 by default) however large the LIMIT, unless iterative scans are switched on.&lt;/p&gt;

&lt;h4&gt;
  
  
  Size the pool for the filter, not for the render
&lt;/h4&gt;

&lt;p&gt;Depth and display count are different quantities. Measure s instead of guessing it: take a sample of real queries, fetch each one deep and unfiltered (a few hundred results), and count what fraction of the top 50 pass the filter. Look at the low end of that distribution, the tenth percentile say, not the mean, because the queries that underfill are the ones with the lowest s. Then size for the tail, since n / s is the depth that works only a little over half the time. When the wanted class is rare but its members sit close to the query, as with the revoked notes, the depth is set by how many non-passing items crowd in above them. The revoked-note floor of 100 cost 0.35 ms.&lt;/p&gt;

&lt;h4&gt;
  
  
  Filter first when the passing set is small
&lt;/h4&gt;

&lt;p&gt;If the filter keeps only a few hundred items, invert the order: select them by the filter, then score each one exactly against the query. A few hundred dot products cost less than embedding the query did, and the answer is exact. It is an easy fix to overlook, because a brute-force loop sitting next to a vector index looks like a step backwards. At that size it is both the faster option and the exact one. On the revoked path above, filtering first would have meant scoring two notes. Engines with a query planner make this switch themselves when a filter is selective enough. The kind-filter backstop is a partial version: it selects by the filter first, but orders by recency instead of scoring.&lt;/p&gt;

&lt;h4&gt;
  
  
  Give each arm its own limit
&lt;/h4&gt;

&lt;p&gt;When a query is a union of conditions with different selectivity, run each as its own bounded query and merge afterwards, so one broad arm can never starve a narrow one. Then look at the cut that comes after the merge. Once the pool is right, the final truncation needs an ordering that reflects what the caller values, or it reproduces the starvation one stage later.&lt;/p&gt;

&lt;h4&gt;
  
  
  Re-query for orderings other than relevance
&lt;/h4&gt;

&lt;p&gt;A sort that is not by similarity needs a candidate set chosen by that sort. Fetch the top rows by the requested key under the same metadata filters, and let membership follow the key, rather than fetching by topic and sorting the result by date.&lt;/p&gt;

&lt;h4&gt;
  
  
  Iterate until satisfied
&lt;/h4&gt;

&lt;p&gt;Fetch, filter, and if the survivors are short and the store has more, fetch deeper and repeat. This is the general solution when selectivity is unknown in advance.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Filtered search with an honest stopping rule&lt;/em&gt;&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;filtered_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_sim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k_max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Nearest n items that pass, or an explicit account of why fewer.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;k_max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k_max&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k_max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;# best first
&lt;/span&gt;        &lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_sim&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;min_sim&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;saw_all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;
        &lt;span class="n"&gt;below_floor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;min_sim&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_sim&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;saw_all&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;below_floor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;                         &lt;span class="c1"&gt;# short, and true
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;k_max&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;               &lt;span class="c1"&gt;# our cap, or the index's
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;returned &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; searched &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k_max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things end the loop: enough survivors, every item seen, the similarity floor crossed, or a cap reached, either the caller's &lt;code&gt;k_max&lt;/code&gt; or one inside the index that returned fewer rows than requested. Only the last produces a short answer that might be wrong, and it is the one that carries a report. Doubling k keeps the total work within about twice the final fetch. Each round keeps its own hits rather than appending pages, because an approximate index searched deeper is not guaranteed to return a superset of what it returned shallower.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt; added exactly this loop inside the database in version 0.8.0, as iterative index scans.&lt;/p&gt;

&lt;h4&gt;
  
  
  The code search already did this
&lt;/h4&gt;

&lt;p&gt;A search pipeline elsewhere in the same system did the right thing without naming it. Before trimming candidates for the reranker, it fetches 200 raw candidates and drops trivial chunks first (one-line templates, fixture stubs, a lone import), so the pool is filled with real code even when a repository carries a hundred or more trivial chunks. It then keeps the first 40 non-trivial candidates from the vector ranking and, separately, the first 40 from the keyword ranking, and reranks the union. That is over-fetch sized for the filter and a separate limit per arm, with the reason written down in the configuration comment. When a language filter is set, that condition travels with the vector query instead, because every chunk's language is stored alongside its vector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtered ANN Search Inside the Index
&lt;/h3&gt;

&lt;p&gt;The strongest fix is a filtered approximate nearest-neighbour search that respects the condition while it walks the index, so the answer is genuinely "the nearest k among items that pass". For graph indexes that is harder than it sounds. An HNSW graph is built so that a greedy walk from an entry point reaches a query's neighbourhood in a few hops, and its edges are chosen by geometry alone. Skip the nodes that fail a selective filter and the walk has fewer usable paths toward the answer, or ends up visiting most of the graph to find the few nodes that pass. (For HNSW from first principles, see the section on approximate search in &lt;a href="https://swapnanilsaha.com/blog/text-embeddings-llms-rag-complete-guide/#sec-ann" rel="noopener noreferrer"&gt;Text Embeddings, Vector Databases and LLMs: A Full Guide&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Two research lines address this directly. &lt;strong&gt;Filtered-DiskANN&lt;/strong&gt; (Gollapudi et al., WWW 2023) builds the graph from the vectors and their labels together, so edges connect points that share a label as well as points that are close. It defines a filter's specificity as the fraction of points carrying the label, and notes that the search-then-filter approach may need a very large number of candidates before it finds a single match when specificity is low. &lt;strong&gt;ACORN&lt;/strong&gt; (Patel et al., SIGMOD 2024) takes the predicate-agnostic route. It builds an HNSW-style graph with each node's candidate neighbour list expanded by a factor γ, then searches only the subgraph induced by the nodes that pass, and it reports 2 to 1,000 times higher throughput than prior methods at fixed recall. The paper sets γ from the inverse of the smallest selectivity to be supported: the same n / s arithmetic, moved from query time into index construction.&lt;/p&gt;

&lt;p&gt;Several engines now document a filter mode that applies during the search. The vocabulary does not line up across them. Elasticsearch and Weaviate call a filter that the graph walk honours as it goes a pre-filter; much of the research literature keeps that word for "apply the filter, then scan the survivors exhaustively" and calls the in-walk version filtered search. The label matters less than two facts about each engine: where the condition meets the ranking, and whether the result can come back with fewer than k rows when enough matches exist. As documented in September 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Filtered query, as documented&lt;/th&gt;
&lt;th&gt;What to watch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pgvector&lt;/td&gt;
&lt;td&gt;With an approximate index (HNSW, IVFFlat), the WHERE condition is applied after the index is scanned. The README's example: a condition matching 10% of rows, with HNSW and the default &lt;code&gt;hnsw.ef_search&lt;/code&gt; of 40, returns about 4 rows on average.&lt;/td&gt;
&lt;td&gt;Iterative index scans (0.8.0 and later) are opt-in: &lt;code&gt;hnsw.iterative_scan&lt;/code&gt; set to &lt;code&gt;strict_order&lt;/code&gt; or &lt;code&gt;relaxed_order&lt;/code&gt;, capped by &lt;code&gt;hnsw.max_scan_tuples&lt;/code&gt; (20,000 by default); &lt;code&gt;ivfflat.iterative_scan&lt;/code&gt; with &lt;code&gt;ivfflat.max_probes&lt;/code&gt;. &lt;code&gt;relaxed_order&lt;/code&gt; can return rows slightly out of distance order; the README re-sorts them with a materialized CTE.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elasticsearch&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;filter&lt;/code&gt; parameter of the &lt;code&gt;knn&lt;/code&gt; query is applied during the approximate search, so k matching documents are returned.&lt;/td&gt;
&lt;td&gt;Every other filter in the query is a post-filter, including a term filter in a bool query's filter clause wrapped around the knn query. That returns fewer than k results even when enough documents match.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qdrant&lt;/td&gt;
&lt;td&gt;Filterable HNSW: extra graph edges built from payload indexes let the search apply filters as it walks the graph.&lt;/td&gt;
&lt;td&gt;Create payload indexes before ingesting data: the filter-aware edges exist only if the graph is built after the indexes, and adding one later means rebuilding. When a condition matches few enough points, the query planner uses a full scan instead of the graph.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weaviate&lt;/td&gt;
&lt;td&gt;Pre-filtering: an allow-list from the inverted index, honoured during the HNSW search. ACORN is the default filter strategy since v1.34; sweeping is the alternative.&lt;/td&gt;
&lt;td&gt;Switches to a flat, brute-force search over the allowed set when the filter is very restrictive.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LanceDB&lt;/td&gt;
&lt;td&gt;Pre-filtering is the default: the &lt;code&gt;where&lt;/code&gt; condition is applied before the vector search.&lt;/td&gt;
&lt;td&gt;Post-filtering is opt-in and can return fewer than &lt;code&gt;limit&lt;/code&gt; rows, or zero, if the nearest neighbours fail the filter.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Each row is taken from the project's own documentation; see Sources. Features in this area change quickly, so check the version you run.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The query text is not the plan. In pgvector, &lt;code&gt;WHERE category = 'x' ORDER BY embedding &amp;lt;-&amp;gt; q LIMIT 10&lt;/code&gt; reads like "filter, then rank", but when the planner uses the HNSW index it scans &lt;code&gt;ef_search&lt;/code&gt; candidates and filters them afterwards. In Elasticsearch, a term filter in a bool query around a knn query reads like a condition on the search, and the documentation is explicit that it is a post-filter. Both return the familiar well-formed short list. Check the execution plan, not the query.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pushing the filter down has a precondition: the index must hold the facts the filter tests. In the memory store behind the four instances, the vectors were written with ids only, and the filters tested columns in a relational table and a lifecycle state computed from an event log. No engine can apply a condition to data it was never given. Two-store designs, vectors in one system and the facts about them in another, are where this bug breeds, and moving the filterable facts next to the vectors is often the real fix.&lt;/p&gt;

&lt;p&gt;Native support also has a ceiling. Iterative scans stop at a configured cap, and a filtered graph search is still approximate. The underfill report from the weakest fix belongs on the strongest one too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 5: Catching It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  An Adversarial Test for Filtered Retrieval
&lt;/h3&gt;

&lt;p&gt;Every test that passes against buggy post-filter code has one property in common: the fetch already reached the answer, so k ≥ r_n held. The store was small enough, or the filter permissive enough, that depth never mattered. The test that catches the bug is the adversarial one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a store where the &lt;strong&gt;nearest neighbours of the test query all fail the filter&lt;/strong&gt;: other workspace, revoked, wrong kind.&lt;/li&gt;
&lt;li&gt;Put the true answer further away, but passing the filter.&lt;/li&gt;
&lt;li&gt;Assert that the true answer is returned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;An adversarial regression test&lt;/em&gt;&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_answer_survives_when_nearest_neighbours_all_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;make_store&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Vectors placed at chosen cosines to the query, so the ranking
&lt;/span&gt;    &lt;span class="c1"&gt;# is fixed by construction rather than by a model's opinion.
&lt;/span&gt;    &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;cosine_stub&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decoy a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decoy b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decoy c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decoy a&lt;/span&gt;&lt;span class="sh"&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;decoy b&lt;/span&gt;&lt;span class="sh"&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;decoy c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a stub embedder. With a real model, whether the decoys outrank the answer is the model's opinion, and a model upgrade can make the test pass for the wrong reason. The vectr regression tests do this in two ways: one helper returns unit vectors at specified cosines to a base vector, another maps marked text to a vector orthogonal to everything else so a note is guaranteed to rank last.&lt;/p&gt;

&lt;p&gt;The tests added with the related-notes, revoked-notes and path-recall fixes, and with the sort fix, all have this shape, and each fails against the code before its fix. The first two get an empty list back. The path-recall test gets a list without the anchored note. The sort test gets a chronological list that starts at the wrong note. The kind-filter fix is covered differently, by tests that pin the pool width and check that the prefetch runs; an outcome test of the adversarial kind would be the stronger guard there.&lt;/p&gt;

&lt;p&gt;That is the regression shape worth keeping: not "does filtering work" but "does filtering still work when the filter removes the closest things".&lt;/p&gt;

&lt;h2&gt;
  
  
  The Line to Keep
&lt;/h2&gt;

&lt;p&gt;"Fetch the nearest k, then filter" answers a different question from "the nearest k that pass the filter", and the difference is invisible because a short answer looks like a complete one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Size the fetch for the filter, give each condition its own limit, and when the result is short, say whether the store ran out or the search did.&lt;/strong&gt; Where the engine can apply the filter during the search, let it, and confirm that it does from the execution plan rather than the query text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;vectr, commit &lt;a href="https://github.com/swapnanil/vectr/commit/77252a6" rel="noopener noreferrer"&gt;77252a6&lt;/a&gt;, "Over-fetch candidates in related_active_notes before post-query filtering". The related-notes fetch depth and the two failing cases (foreign-workspace and revoked nearest neighbours). Code: &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/working_context_store/_related.py" rel="noopener noreferrer"&gt;agent/working_context_store/_related.py&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;vectr, commit &lt;a href="https://github.com/swapnanil/vectr/commit/8f51803" rel="noopener noreferrer"&gt;8f51803&lt;/a&gt;, "decouple revoked-path query depth from revoked_limit". The 2-of-565 revoked-note measurement, the 0.59 ms and 0.94 ms timings on a 50,000-vector collection, and the &lt;code&gt;revoked_query_floor&lt;/code&gt; setting with its rationale in &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/config.yaml" rel="noopener noreferrer"&gt;agent/config.yaml&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;vectr, commit &lt;a href="https://github.com/swapnanil/vectr/commit/24fba5f" rel="noopener noreferrer"&gt;24fba5f&lt;/a&gt;, "recall_for_path declared-trigger arm can no longer starve content/anchor matches". The split into two bounded queries, the relation-strength ordering of the final cut, and the 20 of 20 versus 0 of 20 stress result. Code: &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/working_context_store/_store.py" rel="noopener noreferrer"&gt;agent/working_context_store/_store.py&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;vectr, commit &lt;a href="https://github.com/swapnanil/vectr/commit/24726e4" rel="noopener noreferrer"&gt;24726e4&lt;/a&gt; and the 1.12.0 entry in the &lt;a href="https://github.com/swapnanil/vectr/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;, "Kind-filtered recall no longer under-fills silently". The &lt;code&gt;limit * 10&lt;/code&gt; kind floor and the metadata-only prefetch backstop.&lt;/li&gt;
&lt;li&gt;vectr, commit &lt;a href="https://github.com/swapnanil/vectr/commit/841a149" rel="noopener noreferrer"&gt;841a149&lt;/a&gt; and the 1.11.0 CHANGELOG entry, "An explicit non-relevance recall sort returns the true top-limit". Re-querying under the requested ordering, and the orthogonal-embedding regression test.&lt;/li&gt;
&lt;li&gt;vectr, commit &lt;a href="https://github.com/swapnanil/vectr/commit/77247d6" rel="noopener noreferrer"&gt;77247d6&lt;/a&gt;. The single over-fetch helper, &lt;code&gt;working_memory_fetch_width&lt;/code&gt;, and its stated invariant.&lt;/li&gt;
&lt;li&gt;vectr, &lt;code&gt;ranking.rerank.pre_filter_fetch_k&lt;/code&gt; and &lt;code&gt;top_k_unfiltered&lt;/code&gt; in &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/config.yaml" rel="noopener noreferrer"&gt;agent/config.yaml&lt;/a&gt;, and the trivial-chunk pool filter in &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/searcher.py" rel="noopener noreferrer"&gt;agent/searcher.py&lt;/a&gt;. The code-search over-fetch described under "The code search already did this".&lt;/li&gt;
&lt;li&gt;pgvector, &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;README&lt;/a&gt;, sections "Filtering" and "Iterative Index Scans"; and the &lt;a href="https://www.postgresql.org/about/news/pgvector-080-released-2952/" rel="noopener noreferrer"&gt;pgvector 0.8.0 release announcement&lt;/a&gt;. Filtering after the approximate index scan, the 10% and &lt;code&gt;ef_search&lt;/code&gt; 40 example, and the iterative scan settings.&lt;/li&gt;
&lt;li&gt;Elasticsearch, &lt;a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query" rel="noopener noreferrer"&gt;Knn query reference&lt;/a&gt;, "Pre-filters and post-filters in knn query". Which filters are applied during the approximate search and which after it.&lt;/li&gt;
&lt;li&gt;Qdrant, &lt;a href="https://qdrant.tech/documentation/concepts/indexing/" rel="noopener noreferrer"&gt;Indexing&lt;/a&gt;, "Filterable Index". Payload-index edges in the HNSW graph and the full-scan threshold.&lt;/li&gt;
&lt;li&gt;Weaviate, &lt;a href="https://docs.weaviate.io/weaviate/concepts/filtering" rel="noopener noreferrer"&gt;Filtering&lt;/a&gt; concepts. Allow-list pre-filtering, the ACORN and sweeping strategies, and the flat-search switch for restrictive filters.&lt;/li&gt;
&lt;li&gt;LanceDB, &lt;a href="https://docs.lancedb.com/search/filtering" rel="noopener noreferrer"&gt;Metadata filtering&lt;/a&gt;. Pre-filtering as the default and the documented short results of post-filtering.&lt;/li&gt;
&lt;li&gt;Gollapudi, S., Karia, N., Sivashankar, V., Krishnaswamy, R., Begwani, N., Raz, S., Lin, Y., Zhang, Y., Mahapatro, N., Srinivasan, P., Singh, A., Simhadri, H. V. &lt;a href="https://doi.org/10.1145/3543507.3583552" rel="noopener noreferrer"&gt;Filtered-DiskANN: Graph Algorithms for Approximate Nearest Neighbor Search with Filters&lt;/a&gt;. Proceedings of the ACM Web Conference 2023 (WWW '23), pp. 3406 to 3416. Label-aware graph construction (FilteredVamana, StitchedVamana) and the definition of filter specificity.&lt;/li&gt;
&lt;li&gt;Patel, L., Kraft, P., Guestrin, C., Zaharia, M. &lt;a href="https://doi.org/10.1145/3654923" rel="noopener noreferrer"&gt;ACORN: Performant and Predicate-Agnostic Search Over Vector Embeddings and Structured Data&lt;/a&gt;. SIGMOD 2024, Proceedings of the ACM on Management of Data 2(3); &lt;a href="https://arxiv.org/abs/2403.04871" rel="noopener noreferrer"&gt;arXiv:2403.04871&lt;/a&gt;. Predicate subgraph traversal and the γ neighbour-list expansion.&lt;/li&gt;
&lt;li&gt;Saha, S. &lt;a href="https://swapnanilsaha.com/blog/relevance-is-the-wrong-sort-order/" rel="noopener noreferrer"&gt;Relevance Is the Wrong Sort Order for Agent Memory&lt;/a&gt;. Which ordering memory retrieval should use, and why similarity ranking drops corrections.&lt;/li&gt;
&lt;li&gt;Saha, S. &lt;a href="https://swapnanilsaha.com/blog/text-embeddings-llms-rag-complete-guide/" rel="noopener noreferrer"&gt;Text Embeddings, Vector Databases and LLMs: A Full Guide&lt;/a&gt;. Embeddings, cosine similarity and HNSW search from first principles.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rag</category>
      <category>vectorsearch</category>
      <category>retrieval</category>
      <category>agentmemory</category>
    </item>
    <item>
      <title>Should a Correction Repeat the Mistake?</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Wed, 07 Oct 2026 18:50:54 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/should-a-correction-repeat-the-mistake-10d7</link>
      <guid>https://dev.to/swapnanilsaha/should-a-correction-repeat-the-mistake-10d7</guid>
      <description>&lt;p&gt;To retract a belief, an agent memory can delete it, replace it, or keep it on file labelled false. Only the last can warn an agent about to rediscover the belief elsewhere, and it has to state the belief to do it. Human memory research says the restatement helps. Transformer research says it may prime the error.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Date: 18 September 2026. Reading time: about 28 minutes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When an agent memory learns that one of its beliefs was wrong, it has three options. It can delete the belief. It can replace it with the correct one. Or it can keep a record: this was believed, it turned out false, here is why, do not re-derive it.&lt;/p&gt;

&lt;p&gt;Only the third option can warn an agent that is about to rediscover the wrong belief from somewhere else. It also has a cost the other two avoid. To say what was wrong, it has to say the wrong thing again.&lt;/p&gt;

&lt;p&gt;On whether that repetition helps or hurts, two bodies of research point in opposite directions. Human memory research, after a decade of revising its own advice, says restate the myth: corrections that name the false claim work better, and the feared backfire effect from repeating it does not replicate. Research on transformers suggests the opposite may hold for models. Telling a model not to produce a word can make the word more likely, and the effect grows with the amount of related text that follows the instruction. A coding agent typically reads a correction and then reads a great deal of related text.&lt;/p&gt;

&lt;p&gt;Every memory system's correction format is a bet on one of these answers. Most systems place the bet without knowing there was one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/blog/agents-over-trust-instructions-and-under-trust-corrections/" rel="noopener noreferrer"&gt;Agents Over-Trust Instructions and Under-Trust Corrections&lt;/a&gt; recommends that a correction name the exact belief it overrides. That leaves open how to name it, when the evidence on naming splits by substrate. The misinformation literature supplies the vocabulary: the &lt;em&gt;myth&lt;/em&gt; is the belief being withdrawn, and a &lt;em&gt;retraction&lt;/em&gt; is the statement that withdraws it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Ways an Agent Memory Can Un-Believe Something
&lt;/h2&gt;

&lt;p&gt;An agent memory holds notes that survive between sessions: where a flag is read, which command runs the tests, what the user decided last week. When one of those notes turns out to be wrong, the store has three moves available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delete the wrong note
&lt;/h3&gt;

&lt;p&gt;The note is removed, and the store no longer holds the belief. This is the simplest behaviour to build and a common one. In several memory libraries, each incoming fact is shown to a language model alongside the similar entries already stored, and the model chooses to add it, update an old entry, delete one, or do nothing. A contradicted entry gets deleted or overwritten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replace it with the correct fact
&lt;/h3&gt;

&lt;p&gt;The note is removed and the correct fact is stored in its place. The store now holds only the truth. Nothing records that a different belief ever existed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retract it with a record
&lt;/h3&gt;

&lt;p&gt;The old note stays, marked revoked, with a reason and a date, and the correct fact is stored alongside it. Memory stores deliver notes in two ways: the agent searches for them, or the store injects them into the agent's context on its own when something related comes up. When the revoked note becomes relevant again by either route, what surfaces is not its original content but a retraction:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A retraction as it reaches the agent (illustrative values)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do not re-derive this from other sources without verification. Previously believed (recorded 2026-07-14, revoked 2026-07-21, reason: the flag moved to the settings service in 2.3): "Feature flags are read from config/flags.yaml".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shape is the template that &lt;a href="https://github.com/swapnanil/vectr" rel="noopener noreferrer"&gt;vectr&lt;/a&gt;, an open-source memory store I maintain, uses for revoked notes. The dates, reason and claim above are illustrative. The template itself, verbatim from &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/working_context_store/_store.py#L633" rel="noopener noreferrer"&gt;&lt;code&gt;agent/working_context_store/_store.py&lt;/code&gt;&lt;/a&gt;:&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="n"&gt;_ANTI_MEMORY_TEMPLATE&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;Do not re-derive this from other sources without verification. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Previously believed (recorded {created_date}, revoked {revoked_date}, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reason: {reason}): &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{summary}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Temporal knowledge graphs sit between the second and third options. A superseded fact gets an invalidation timestamp and stays queryable as history, but nothing injects it; somebody has to go looking.&lt;/p&gt;

&lt;p&gt;The options also differ in storage cost and in how easy they are to audit or undo. For agents the axis that decides between them is a different one: &lt;strong&gt;what happens when the wrong belief comes back from outside the store.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A publisher finds an error on page 12 of a reference manual. Pulling the book from the shelf (delete) does nothing about the photocopies already circulating. Printing a corrected edition (replace) helps whoever picks up the new edition, but a reader holding an old photocopy has no reason to doubt it. An errata slip (retract) is the only option that speaks to the reader of the photocopy, and it can only do so by quoting the error: page 12 says X; X is wrong; the correct value is Y.&lt;/p&gt;

&lt;p&gt;Where the analogy falls short is delivery. An errata slip only helps a reader who sees it, and for an agent, where and when the retraction is delivered turns out to matter as much as what it says.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Deleting a Wrong Memory Does Not Remove the Belief
&lt;/h2&gt;

&lt;p&gt;The case for deletion rests on an assumption: removing the stored belief removes the belief. It fails in people and in model weights, where it has been tested directly, and it fails more obviously still in an agent's workspace.&lt;/p&gt;

&lt;h3&gt;
  
  
  In people: directed forgetting
&lt;/h3&gt;

&lt;p&gt;In directed forgetting experiments, participants study items and are then told to forget some of them. Forget-cued items are recalled less often, but the evidence says they are still stored. In the list-method version of the task, the forgetting shows up on recall and typically vanishes on a recognition test, where the item is shown again (Abel et al., 2021, summarize the pattern). Current reviews attribute the effect to control of attention during study and to a deliberate shift of mental context (Delaney et al., 2020). None of the explanations on offer involves anything being removed; the item has become harder to reach while staying in storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  In model weights: editing and unlearning
&lt;/h3&gt;

&lt;p&gt;Knowledge editing changes a single fact inside a model's weights. The RippleEdits benchmark (Cohen et al., TACL 2024) checks the fact's consequences as well as the fact itself: edit who someone's parent is, then ask about their siblings. Current editing methods fail to make those consequences consistent. The edited fact moves; the facts that depend on it often do not. The best score on that benchmark came from the simplest baseline, in-context editing: putting the new fact in the prompt, which is what an agent memory does.&lt;/p&gt;

&lt;p&gt;Machine unlearning does worse. (Retraining from scratch without the data does remove it; it is also too expensive to do per fact, which is why the approximate methods below exist.) Zhang et al. (ICLR 2025) found that for unlearning methods with utility constraints, models still retain on average 21% of the knowledge they were meant to forget at full precision, and 83% after 4-bit quantization. Hu et al. (ICLR 2025) showed that fine-tuning an unlearned model on a small, loosely related public dataset brings the unlearned content back; general wiki text about a book series was enough to make a model reproduce memorized passages of the books verbatim. Whatever the unlearning step did to the weights, it left enough behind for a small amount of training on adjacent text to bring the passages back.&lt;/p&gt;

&lt;h3&gt;
  
  
  In an agent's workspace
&lt;/h3&gt;

&lt;p&gt;For a coding agent, the belief was never only in the memory store. It was in the documentation, in comments, in a vendored copy of a config, in twenty call sites that still use the old API. Deleting the note deletes the only object that knew the belief was wrong. The next session reads the stale README, re-derives the old fact, acts on it, and may well write it into a new file, where the session after that will find it.&lt;/p&gt;

&lt;p&gt;Research on unlearning in agents has named a narrower version of this loop. Wang et al. (2026) call it &lt;strong&gt;parameter-memory backflow&lt;/strong&gt;: an agent has its content removed from the model's weights and from its memory store, and the content comes back anyway, because retrieval reactivates what is left in the weights, or because derived artifacts (indices, summaries, embeddings, caches) still carry it and reintroduce it. Their loop runs between the weights and the memory store. A workspace runs the same kind of loop through many more places, and no unlearning method reaches any of them.&lt;/p&gt;

&lt;p&gt;Deletion and replacement both leave the workspace's stale copies unopposed. Replacement at least stores the truth, but the truth arrives as one note, with no indication that the workspace disagrees with it and that the workspace is the one that is wrong. Only a retraction record says so. Whether saying so helps is the open question.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Backflow in a workspace does not need the model to remember anything. The stale README is enough. That makes it the common case rather than an exotic one: any fact that changed in code but not in prose can be re-derived by the next session that reads the prose, and a store that deleted its note has nothing left to object with.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Human Answer: A Correction Should Name the Myth
&lt;/h2&gt;

&lt;p&gt;The misinformation literature measures the quantity at stake here directly: how much people keep relying on information after it has been retracted. Its most argued-over practical question for the last decade is whether a correction should repeat the myth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retractions alone are weak: the continued influence effect
&lt;/h3&gt;

&lt;p&gt;The continued influence effect is one of the most replicated findings in the field. In the standard paradigm (Johnson and Seifert, 1994), people read a report of a warehouse fire said to be caused by gas cylinders and oil paints stored in a closet. Some then read a retraction: the closet was actually empty. Asked later what caused the black smoke, many still cite the paint.&lt;/p&gt;

&lt;p&gt;The major review of the literature (Lewandowsky et al., 2012) is blunt: retractions "rarely, if ever, have the intended effect of eliminating reliance on misinformation, even when people believe, understand, and later remember the retraction." A retraction at most halves the references to the misinformation compared with no retraction, and in some studies it does not reduce them at all. Delivering it immediately, inside the same story, does not fix this. Making the negation more emphatic ("paint and gas were never on the premises") made reliance worse in one study. The review found only three things that reliably help: a warning before exposure, repeating the retraction, and an alternative account that fills the gap the retraction leaves.&lt;/p&gt;

&lt;h3&gt;
  
  
  A warning plus an alternative works best
&lt;/h3&gt;

&lt;p&gt;Ecker, Lewandowsky and Tang (2010) tested warnings directly. A specific warning, one that explained the continued influence effect to participants up front, reduced reliance on the retracted claim without eliminating it. A general warning (news is not always fact-checked before publication) did even less. In a further experiment, a specific warning combined with a plausible alternative explanation reduced reliance further, and still did not eliminate it.&lt;/p&gt;

&lt;p&gt;The accepted account is causal. People keep a false belief when removing it would leave a hole in their model of what happened; the retraction says what did &lt;em&gt;not&lt;/em&gt; cause the smoke and leaves the smoke unexplained, so the paint stays in use as the best explanation available. The alternative has to do real work: it should cover the same facts the myth explained, ideally say why the myth seemed right in the first place, and not be more complicated than the myth it replaces (Lewandowsky et al., 2012).&lt;/p&gt;

&lt;h3&gt;
  
  
  Repeating the myth does not backfire
&lt;/h3&gt;

&lt;p&gt;For years the practical advice was never to repeat the myth, on the theory that repetition makes a claim familiar and familiarity is mistaken for truth. The 2012 review itself advised emphasizing the facts rather than the myth for that reason. The familiarity backfire has not held up.&lt;/p&gt;

&lt;p&gt;Ecker, Hogan and Lewandowsky (2017) found that retractions which explicitly repeated the misinformation reduced reliance on it more than retractions that avoided repeating it, and attribute the gain to salience: a correction that names its target is easier to notice as a correction. Ecker, Lewandowsky and Chadwick (2020) then tested the case in which backfire should be most likely: correcting a myth the audience had never heard, so that the correction itself was the first exposure. Across three experiments and a one-week delay, they found substantial evidence against a familiarity backfire. &lt;em&gt;The Debunking Handbook 2020&lt;/em&gt;, written by 22 researchers in the area, concludes that "repeating a myth while refuting it has been found to be safe in many circumstances, and can even make the correction more salient and effective."&lt;/p&gt;

&lt;p&gt;The new-audience case is the normal one for agents. A fresh session that has not yet opened the stale README meets the myth for the first time in the retraction.&lt;/p&gt;

&lt;h3&gt;
  
  
  A candidate mechanism: memory reconsolidation
&lt;/h3&gt;

&lt;p&gt;There is a mechanistic reason naming the myth might be necessary rather than merely safe, though it comes from a different literature and should be held loosely. Nader, Schafe and LeDoux (2000) showed in rats that a consolidated fear memory, when reactivated by retrieval, becomes unstable again and has to be re-stored. Injecting a drug that blocks protein synthesis (which the brain needs to re-store it) right after reactivation removed the fear response; the same injection without reactivation left the memory intact. The idea that retrieval opens a memory to updating, called reconsolidation, has since been pursued in humans, and whether it governs everyday belief revision is still debated (Lee, Nader and Schiller, 2017, review both the evidence and the challenges). As a mechanism, it suggests why a correction that never names what it corrects might have nothing to attach to. None of the misinformation findings above depend on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Corrections decay unless revisited
&lt;/h3&gt;

&lt;p&gt;When people get corrective feedback, the errors they held with the most confidence are the most likely to be fixed on a retest. That is the hypercorrection effect (Butterfield and Metcalfe, 2001). The fix does not always last. Butler, Fazio and Marsh (2011) found that after a week, correction rates fell, and the errors that came back were disproportionately the high-confidence ones. Metcalfe and Miele (2014) found that a test given immediately after the corrective feedback, requiring people to produce the right answer themselves, both improved delayed memory for the answer and blocked the old errors from returning. Metcalfe's own later review calls the evidence on error return not yet definitive. I would still design around the direction: a correction seen once fades, and the confidently held error is the one most likely to come back.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Debunking Handbook 2020&lt;/em&gt; condenses the practical advice into a four-part recipe for a written correction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fact&lt;/strong&gt;: lead with the truth, if it is short and clear&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Myth&lt;/strong&gt;: warn it is coming, then state it once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallacy&lt;/strong&gt;: explain how the myth misleads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fact&lt;/strong&gt;: finish on the truth, repeated if possible&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;em&gt;fallacy&lt;/em&gt; step is where the alternative account lives. The Handbook's instruction is to explain why the mistaken information was thought to be correct in the first place, why it is now clear it is wrong, and why the alternative is correct. It is also blunt about the minimal version: "Do not rely on a simple retraction ('this claim is not true')."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Transformer Answer: Naming the Error May Prime It
&lt;/h2&gt;

&lt;p&gt;The research on language models points the other way, and for a reason that has nothing to do with familiarity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ironic rebound in transformers
&lt;/h3&gt;

&lt;p&gt;In people, trying not to think of a white bear makes the bear come to mind; Wegner and colleagues (1987) called it a paradoxical effect of thought suppression, and it is now known as ironic rebound. Mann et al. (2025) looked for the same effect in transformers. Their benchmark, ReboundBench, contains 5,000 prompts built around the instruction "do not mention X", each followed by a stretch of distractor text that varies in length and kind. They measure how likely the model is to produce X next, against a matched prompt with no negation.&lt;/p&gt;

&lt;p&gt;Across nine open-weight models, they report that rebound "consistently arises immediately after negation and intensifies with longer or semantic distractors." The forbidden word becomes more likely, not less, and the effect is strongest when the text after the instruction is on the same topic; repetitive filler supported suppression instead. A circuit analysis traced the effect to a sparse set of middle-layer attention heads that amplify the forbidden token while earlier layers suppress it. The paper's own framing of why: suppressing a concept "requires internally activating it, which may prime rebound instead of avoidance."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Math · what rebound measures&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The paper's suppression score, in simplified form, for a forbidden word X and L tokens of distractor text between the instruction and the measurement point:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rebound(L) = log₂ p(X | "do not mention X" + L tokens)
          − log₂ p(X | neutral context + L tokens)
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Here p(X | ...) is the model's next-token probability for X. A positive value means the instruction not to mention X made X more likely than it would have been with no instruction at all; each unit is a doubling of the probability. The paper converts L to percentiles within each model's range of distractor lengths, to account for different context windows, so "longer" is relative to the model. Its headline surprisal measure compares against a high-load baseline instead of a neutral prompt; the sign convention is the same.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The models ranged from GPT-2 Small to two 20-billion-parameter models. The smaller models showed only a brief rebound before suppression took over; the mid-sized ones sustained the strongest effect. One of the largest, GPT-OSS-20B, showed minimal or even negative rebound, which the authors note breaks the scaling trend. That matters here: the models behind most coding agents are, by most estimates, far larger than anything tested, and the one data point at the top of the range points the other way. The measure is also narrow: the probability of a single-token common noun in templated, lowercased prompts, not the behaviour of an agent editing code. Carrying the result over to a retraction in an agent's context is an extrapolation, and I would treat it as a hypothesis worth guarding against rather than a known cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Negation is weak in general
&lt;/h3&gt;

&lt;p&gt;García-Ferrero et al. (EMNLP 2023) built a dataset of about 400,000 commonsense sentences, two-thirds of them containing negation, and found that large language models classify affirmative sentences well but struggle with negated ones, often relying on surface cues rather than on the negation itself. A retraction is, structurally, a negation wrapped around a quoted claim.&lt;/p&gt;

&lt;h3&gt;
  
  
  Knowledge conflicts: coherence and agreement beat truth
&lt;/h3&gt;

&lt;p&gt;Xie et al. (ICLR 2024) studied what models do when evidence in the prompt conflicts with what they learned in training, their parametric memory. Three results bear on retractions. Models readily accept contradicting evidence when it is coherent and convincing. They favour evidence that agrees with what they already believe. And when several passages are in context, they tend to side with whichever answer more passages support; the paper's section heading is "LLMs follow the herd and choose the side with more evidence."&lt;/p&gt;

&lt;p&gt;A fourth detail, from the paper's appendix, bears on the length of a correction. When the contradicting evidence was cut down to a bare answer with no supporting explanation, ChatGPT's adoption of it on PopQA questions fell from 56.7% to 18.8%. Cutting the evidence that agreed with the model's prior to a bare answer did nothing of the kind; it was chosen slightly more often (42.7% to 43.9%). So agreement survives being shortened, and contradiction does not.&lt;/p&gt;

&lt;p&gt;In an agent's context, the quoted false claim agrees with the stale README, the stale comments and the old call sites. If the model weighs text by coherence and agreement, the quote risks counting as one more vote for the wrong answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every injected sentence has a cost
&lt;/h3&gt;

&lt;p&gt;Shi et al. (ICML 2023) took grade-school maths problems that the models could already solve and added one irrelevant sentence to each. Of the problems solved without the extra sentence, no more than 18% stayed solved across every type of distractor. Sentences on the same topic as the problem, reusing its character names, did the most damage. An explicit instruction to ignore irrelevant information recovered some of the loss. For a memory store, this is the argument for gating delivery on relevance: a retraction injected into a session where the old belief was never going to come up can only distract.&lt;/p&gt;

&lt;p&gt;The agent setting combines these conditions. An agent receives a retraction and then, in the same context window, reads a long, topically related document that asserts the retracted claim. That is close to the condition in which the rebound study found its strongest effect: a negation followed by a large amount of related text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Both Answers Can Be Right
&lt;/h2&gt;

&lt;p&gt;The two literatures study different mechanisms under different conditions, so their answers can both hold.&lt;/p&gt;

&lt;p&gt;In the human experiments, a person reads a story, reads a correction, and later answers questions. Processing the retraction attaches something like a "false" tag to the memory; the 2012 review calls it a negation tag. The later question retrieves the memory together with its tag. The retracted claim is not in front of the person at the moment of use. It is recalled, and recall brings the correction along.&lt;/p&gt;

&lt;p&gt;The human literature knows the failure mode of that tag, too. Johnson and Seifert (1998) found that a corrected detail was still activated in memory whenever the story referred back to it. The negation tag can also be lost under cognitive load (Gilbert, Krull and Malone, 1990), leaving the misinformation behind on its own. Negations hold best when they can be re-encoded as a positive statement: people told someone is "not messy" tend to remember "tidy", while "not charismatic", which has no ready opposite, drifts back towards "charismatic" (Mayo, Schul and Burnstein, 2004). For retraction design this is the most useful finding in the human literature, because it transfers directly: a retraction that can be read as "use Y" holds up better than one that can only be read as "not X".&lt;/p&gt;

&lt;p&gt;In the agent case, the retracted claim is &lt;strong&gt;in the context window, as tokens, at the moment of use&lt;/strong&gt;, next to other copies of the same claim from the workspace. There is no separate retrieval step that fetches a memory with its tag attached. The negation has to win against the quoted content, in attention, at every step where the claim is relevant.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Analogy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The human case is a filing cabinet. The correction is stamped on the card, and pulling the card out brings the stamp with it. The model case is a meeting where every document is read aloud at once: the retraction is one voice saying "not X", while the README and three old call sites are four voices saying "X", and nothing is filed anywhere.&lt;/p&gt;

&lt;p&gt;The analogy overstates one thing: attention is not a vote count. Xie et al.'s herd result suggests it behaves like one often enough to matter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the human research answers the question "does naming the myth help the correction get remembered and used?", while the transformer research asks "does naming the myth add activation to the myth?" Both can be true at once. For a memory designer, the practical question is which effect dominates in a real agent session, and &lt;strong&gt;that has not been measured.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing a Retraction for Agent Memory That Hedges Both Bets
&lt;/h2&gt;

&lt;p&gt;On several design questions the two literatures agree, and those choices are easy. On the rest, a retraction can be shaped so that if the transformer account turns out to be right, the damage is small.&lt;/p&gt;

&lt;h3&gt;
  
  
  Carry the alternative, always
&lt;/h3&gt;

&lt;p&gt;A warning with no alternative is the weakest correction in the human data, because it leaves a causal gap. The transformer studies above did not test warnings as such, but the closest result points the same way: Xie et al.'s bare-answer finding says a short contradiction with nothing supporting it is the kind of evidence a model ignores. A retraction's reason field should state what is true now and what replaced the old belief, not only that the old one is wrong.&lt;/p&gt;

&lt;p&gt;The human evidence adds a sharper requirement: the alternative should explain why the myth looked right. In a workspace, that explanation is nearly always some version of "the code moved and the docs did not". A reason that says so answers the stale file before the agent opens it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two reasons for the same retraction&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bare:       reason: no longer true
corrective: reason: the flag moved to the settings service in 2.3; read flags
            with settings.flag(name); docs/flags.md predates the move and was
            never updated; verify with ./tools/settings flags list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keep the restated claim short
&lt;/h3&gt;

&lt;p&gt;The myth should appear once, briefly. In vectr, the retraction quotes only the note's one-line title, never its full content; when the writer gave no title, the title is the note's first line cut at 80 characters. The reason is the unbounded field. So the part that competes with the truth is small and the part that carries it can be as long as it needs. The 80-character cap is a convention; nobody has measured where a longer quote starts to cost more than it helps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decide what survives truncation
&lt;/h3&gt;

&lt;p&gt;Injection budgets are small, and a long retraction gets cut, usually from the right. The order of the fields decides what is lost first. In vectr, the template was reordered (&lt;a href="https://github.com/swapnanil/vectr/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;: "The revoked-note deterrent survives truncation") so that right-truncation drops the quoted claim first, then the reason, and keeps the warning longest. When the budget is tight, the myth goes before anything else does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not end on the myth
&lt;/h3&gt;

&lt;p&gt;Here the human literature argues against that same template. Untruncated, it ends on the quoted false claim, and the debunking recipe ends on the fact. A template that puts the warning first, then the claim, then a reason that carries the replacement, ending on the replacement, would satisfy both the warning-first constraint and the human evidence, provided the claim is short enough that the reason still fits.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning · the truncation trap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proviso breaks down under tight budgets. In the fact-last order, right-truncation cuts the replacement before it touches the claim, so under a tight budget the retraction degrades to a warning plus the myth: the combination both literatures rate worst. With a plain character cut, no field order gets both properties, myth lost first and fact kept last. Field-aware truncation gets both: measure the rendered record against the budget before sending it, and if it does not fit, drop the quoted claim as a whole unit, then optional detail such as the verify hint, and only then cut into the reason. If I had to pick one change from this section to ship first, it would be this one. Reordering fields only changes which field breaks first under a tight budget, while checking the rendered record against the budget stops a broken one from being sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive demo: A Retraction Under a Budget&lt;/strong&gt; (runs on the &lt;a href="https://swapnanilsaha.com/blog/should-a-correction-repeat-the-mistake/" rel="noopener noreferrer"&gt;original page&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Pick a template order and a reason, then shrink the budget. The point to observe: with character-level truncation, whatever sits at the end of the template is lost first, so a record that ends on the replacement loses the replacement first. Switch to field-aware truncation and the order can serve the reader while the budget only decides whether the myth appears at all.&lt;/p&gt;

&lt;p&gt;Illustrative record; the claim, dates, command and service names are invented. Character mode cuts at the exact character. Real implementations usually back off to a word or sentence boundary, which moves the cut a little and does not change which field goes first. Field-aware mode drops, in order: the quoted claim, the verify hint, the "why it looked right" clause, then cuts from the right only if the warning and replacement alone still exceed the budget.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Make it checkable
&lt;/h3&gt;

&lt;p&gt;A retraction the agent can verify with one command turns passive reading into an action. In the human studies, the corrections that held over a delay were the ones followed by a test, where the learner had to produce the answer (Metcalfe and Miele, 2014). A verify hint is the nearest agent equivalent: the agent runs it and observes the result instead of taking the note's word. &lt;a href="https://swapnanilsaha.com/blog/agents-over-trust-instructions-and-under-trust-corrections/" rel="noopener noreferrer"&gt;Agents Over-Trust Instructions and Under-Trust Corrections&lt;/a&gt; argues that a verify hint is the cheapest way to make a correction carry evidence rather than authority. It also has a transformer-side rationale: fresh command output is the kind of coherent, well-supported evidence Xie et al. found models willing to accept.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deliver it more than once, across sessions
&lt;/h3&gt;

&lt;p&gt;If corrected errors return with time, a retraction visible only on request (the temporal-graph pattern) will not be seen when it matters. The agent about to re-derive the old belief has no reason to query for retractions of a belief it thinks is simply true. For an agent the problem is sharper than for a person, because nothing carries over between sessions on its own. A person who saw a correction last week retains a weakened trace of it; a new session that was not handed the retraction has no trace at all.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design rule&lt;/th&gt;
&lt;th&gt;Human evidence&lt;/th&gt;
&lt;th&gt;Transformer evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Carry the alternative&lt;/td&gt;
&lt;td&gt;Warning plus alternative beats warning alone (Ecker et al., 2010); gap-filling accounts&lt;/td&gt;
&lt;td&gt;Bare contradictions are rarely adopted (Xie et al., 2024)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep the myth short&lt;/td&gt;
&lt;td&gt;State it once only (Debunking Handbook 2020)&lt;/td&gt;
&lt;td&gt;Fewer tokens of the myth means less to amplify (inference, not measured)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field-aware truncation&lt;/td&gt;
&lt;td&gt;A myth with no alternative is the weakest correction&lt;/td&gt;
&lt;td&gt;A negation with nothing else to attend to is the weakest case (inference from the rebound and bare-answer results)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End on the fact&lt;/td&gt;
&lt;td&gt;The debunking recipe ends on the fact&lt;/td&gt;
&lt;td&gt;Evidence order matters, but its direction varies by model (Xie et al., 2024)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make it checkable&lt;/td&gt;
&lt;td&gt;Testing after feedback blocks error return (Metcalfe and Miele, 2014)&lt;/td&gt;
&lt;td&gt;Coherent in-context evidence is accepted (Xie et al., 2024)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deliver across sessions&lt;/td&gt;
&lt;td&gt;Corrected high-confidence errors return after a delay (Butler et al., 2011)&lt;/td&gt;
&lt;td&gt;No carry-over between sessions: undelivered means absent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to Find Out: A Four-Arm Retraction Experiment
&lt;/h2&gt;

&lt;p&gt;Which effect dominates in a real session can only be settled by running sessions. An experiment designed to do that is public in the vectr repository as &lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/anti_memory/DESIGN.md" rel="noopener noreferrer"&gt;&lt;code&gt;benchmarks/anti_memory/DESIGN.md&lt;/code&gt;&lt;/a&gt;. It has four arms, which differ only in what the store holds and whether it injects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arm&lt;/th&gt;
&lt;th&gt;Store holds&lt;/th&gt;
&lt;th&gt;Delivery&lt;/th&gt;
&lt;th&gt;Stands in for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;td&gt;Injected (nothing to inject)&lt;/td&gt;
&lt;td&gt;Dropping or overwriting the conflicting note&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit&lt;/td&gt;
&lt;td&gt;Retraction record plus the correct fact&lt;/td&gt;
&lt;td&gt;On request only&lt;/td&gt;
&lt;td&gt;Temporal history you can query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace&lt;/td&gt;
&lt;td&gt;The correct fact only&lt;/td&gt;
&lt;td&gt;Injected&lt;/td&gt;
&lt;td&gt;Updating the note in place&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retract&lt;/td&gt;
&lt;td&gt;Retraction record plus the correct fact&lt;/td&gt;
&lt;td&gt;Injected&lt;/td&gt;
&lt;td&gt;A warning-first retraction record&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each scenario is authored so the workspace still teaches the old fact and the task naturally leads the agent to read it. In one, a feature-flag API has moved: the docs still teach the old call over a YAML file, three legacy modules still use it, and the live path, over a directory of TOML files, is one file away. The majority vote in the workspace points at the old fact. All workspaces are synthetic, invented for the experiment, so nothing in the model's training can supply either fact.&lt;/p&gt;

&lt;p&gt;The measured outcome is mechanical. Each session's final files are parsed and the session is scored as &lt;em&gt;backflow&lt;/em&gt; (they encode the old fact), &lt;em&gt;correct&lt;/em&gt; (they encode the new one) or &lt;em&gt;neither&lt;/em&gt;. No language model judges a run. Mechanical is not automatically correct, though. A scorer that matches a command pattern anywhere in a shell string will count a &lt;code&gt;grep&lt;/code&gt; that merely mentions the command as having run it, a failure documented in &lt;a href="https://swapnanilsaha.com/research/under-trusted-memory/" rel="noopener noreferrer"&gt;Deterministic, Reproducible, and Wrong&lt;/a&gt;. This design anchors its command matches to execution position for that reason.&lt;/p&gt;

&lt;p&gt;Two comparisons carry the result. &lt;strong&gt;Retract against audit&lt;/strong&gt; asks whether pushing the record matters. &lt;strong&gt;Retract against replace&lt;/strong&gt; asks whether the record adds anything beyond stating the truth. The second is the one that can kill the idea, and the design treats a loss there as a publishable result.&lt;/p&gt;

&lt;p&gt;The comparison has a built-in confound, and the design states it. Retract delivers strictly more text than replace, the same corrective note plus the retraction, so a win for retract is tangled up with injected volume. Every session records the characters injected, and a length-matched placebo arm (replace plus an irrelevant note of the same length) is priced to run only if retract beats replace. The design also records the opposite reading. On the human account, the extra text in the retract arm is the active ingredient, a warning plus an alternative, and calling it padding gets the mechanism backwards. The placebo is what tells the two readings apart: if an irrelevant note of equal length does as well, the content was not doing the work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Math · the decision rule&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let B_X be the number of sessions, out of three per arm, scored as backflow in arm X.&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;G0 (the scenario tempts):   B_delete  ≥ 2
G1 (pushing matters):       B_retract ≤ B_audit   − 2
G2 (the record adds value): B_retract ≤ B_replace − 2
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Supported requires G0, G1 and G2. Refuted is G0 with retract doing no better than audit or no better than replace. Anything else is inconclusive, which permits exactly one escalation, to six sessions per arm with the margin scaled to four. With three sessions per arm, this detects large effects only, and the single escalation is the design's admission of that.&lt;/p&gt;

&lt;p&gt;G0 is a gate on the scenario, not a result: if deleting the note does not lead most sessions back to the old fact, the workspace was not tempting enough and the scenario is replaced. One more case is carved out in advance. If replace already fixes every session (B_replace = 0 with B_delete = 3), G2 has no room to show anything and is reported as uninformative rather than as a loss. The design requires the rule to be frozen in a file whose hash is stamped into every result, so it cannot quietly change after the data arrives.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The rebound signature
&lt;/h3&gt;

&lt;p&gt;The design also includes a free diagnostic for the transformer hypothesis. If a session ships the old fact &lt;strong&gt;without ever having read a stale artifact&lt;/strong&gt;, the only text in its context that states the old fact is the retraction itself. That is the rebound signature, and it separates "the workspace out-voted the correction" from "the correction taught the myth."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Math · rebound signature&lt;/strong&gt;&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rebound_signature = backflow_shipped AND NOT stale_read
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Here &lt;code&gt;backflow_shipped&lt;/code&gt; means the session was scored as backflow, and &lt;code&gt;stale_read&lt;/code&gt; means it read or searched any file that still teaches the old fact.&lt;/p&gt;

&lt;p&gt;Three conditions make it readable. The facts are synthetic, so training cannot supply them. The signature means nothing in a scenario with no stale artifact at all, where every failure trivially satisfies NOT stale_read; the design restricts rebound claims to scenarios that have one. And a model can still land on the old fact by guessing from convention (a flags file in YAML is a common layout), so a single session with the signature proves little. What counts is the rate, compared across arms: the retract arm is the only one that injects the old fact into every session, so a higher rate there than in replace is the evidence. Stale reads are matched broadly, which biases the diagnostic against false rebound claims rather than towards them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The decision rule and the predicted directions are written down in public before any data. A harness implementing the design is in the same directory. None of it has been run: at publication, the design document's status line reads "DESIGN (no cells run, no product code changed)", where a cell is one agent session in one arm.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Each Account Predicts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prediction 1 (human account).&lt;/strong&gt; Retract beats replace, and the gap grows with how much stale material the workspace contains and with the number of sessions the retraction is delivered across.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 2 (transformer account).&lt;/strong&gt; Backflow in the retract arm will include sessions with no stale read at all, concentrated early, shortly after the retraction is injected. If the rebound signature is more common in the retract arm than in the replace arm, the mechanism is partly causing the failure it is meant to prevent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 3 (both).&lt;/strong&gt; A bare retraction with no alternative in its reason will do no better than replacement alone, and a retraction whose reason carries the replacement will do markedly better than a bare one: a step, not a gradual slope. The design tests this with three reasons attached to the same quoted claim, from bare ("no longer true") through causal (what changed and when) to corrective (what is true now, plus a verify command).&lt;/p&gt;

&lt;p&gt;A loss for retract would not make retraction records useless. They would still serve audits and the humans reading them. It would mean that pushing the myth into an agent's context costs more than it buys, and that the record belongs on the query path, not in the injection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Line to Keep
&lt;/h2&gt;

&lt;p&gt;A memory that deletes a wrong belief leaves every stale copy of it unopposed. A memory that keeps a retraction has to repeat the mistake to warn against it. People are helped by that repetition, and transformers may be primed by it. Keep the myth short and early, give the alternative the space, and measure whether the warning is teaching the thing it forbids.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Lewandowsky, S., Ecker, U. K. H., Seifert, C. M., Schwarz, N., Cook, J. &lt;a href="https://doi.org/10.1177/1529100612451018" rel="noopener noreferrer"&gt;Misinformation and Its Correction: Continued Influence and Successful Debiasing&lt;/a&gt;. &lt;em&gt;Psychological Science in the Public Interest&lt;/em&gt; 13(3), 2012, pp. 106-131. The standard review of the continued influence effect: the warehouse-fire paradigm, the "at most halve" figure, the three factors that help, the negation-tag account, and the alternative-account requirements.&lt;/li&gt;
&lt;li&gt;Johnson, H. M., Seifert, C. M. &lt;a href="https://doi.org/10.1037/0278-7393.20.6.1420" rel="noopener noreferrer"&gt;Sources of the continued influence effect: When misinformation in memory affects later inferences&lt;/a&gt;. &lt;em&gt;Journal of Experimental Psychology: Learning, Memory, and Cognition&lt;/em&gt; 20(6), 1994, pp. 1420-1436. The original demonstration of continued influence after an immediate retraction.&lt;/li&gt;
&lt;li&gt;Johnson, H. M., Seifert, C. M. &lt;a href="https://doi.org/10.1037/0278-7393.24.6.1483" rel="noopener noreferrer"&gt;Updating accounts following a correction of misinformation&lt;/a&gt;. &lt;em&gt;Journal of Experimental Psychology: Learning, Memory, and Cognition&lt;/em&gt; 24(6), 1998, pp. 1483-1494. Corrected misinformation stays active during later reading.&lt;/li&gt;
&lt;li&gt;Ecker, U. K. H., Lewandowsky, S., Tang, D. T. W. &lt;a href="https://doi.org/10.3758/MC.38.8.1087" rel="noopener noreferrer"&gt;Explicit warnings reduce but do not eliminate the continued influence of misinformation&lt;/a&gt;. &lt;em&gt;Memory &amp;amp; Cognition&lt;/em&gt; 38(8), 2010, pp. 1087-1100. Specific versus general warnings, and a specific warning combined with a plausible alternative explanation.&lt;/li&gt;
&lt;li&gt;Ecker, U. K. H., Hogan, J. L., Lewandowsky, S. &lt;a href="https://doi.org/10.1016/j.jarmac.2017.01.014" rel="noopener noreferrer"&gt;Reminders and Repetition of Misinformation: Helping or Hindering Its Retraction?&lt;/a&gt; &lt;em&gt;Journal of Applied Research in Memory and Cognition&lt;/em&gt; 6(2), 2017, pp. 185-192. Retractions that explicitly repeat the misinformation are more effective than ones that avoid it.&lt;/li&gt;
&lt;li&gt;Ecker, U. K. H., Lewandowsky, S., Chadwick, M. &lt;a href="https://doi.org/10.1186/s41235-020-00241-6" rel="noopener noreferrer"&gt;Can corrections spread misinformation to new audiences? Testing for the elusive familiarity backfire effect&lt;/a&gt;. &lt;em&gt;Cognitive Research: Principles and Implications&lt;/em&gt; 5, 41, 2020. Three experiments, N = 1718, one-week delay: no familiarity backfire when correcting novel misinformation.&lt;/li&gt;
&lt;li&gt;Lewandowsky, S., Cook, J., Ecker, U. K. H., et al. &lt;a href="https://sks.to/db2020" rel="noopener noreferrer"&gt;The Debunking Handbook 2020&lt;/a&gt;. DOI: 10.17910/b7.1182. Consensus guide by 22 researchers; the fact, myth, fallacy, fact recipe, the warning against a simple retraction, and the quoted conclusion on myth repetition.&lt;/li&gt;
&lt;li&gt;Gilbert, D. T., Krull, D. S., Malone, P. S. &lt;a href="https://doi.org/10.1037/0022-3514.59.4.601" rel="noopener noreferrer"&gt;Unbelieving the unbelievable: Some problems in the rejection of false information&lt;/a&gt;. &lt;em&gt;Journal of Personality and Social Psychology&lt;/em&gt; 59(4), 1990, pp. 601-613. False-tagged information loses its tag under cognitive load.&lt;/li&gt;
&lt;li&gt;Mayo, R., Schul, Y., Burnstein, E. &lt;a href="https://doi.org/10.1016/j.jesp.2003.07.008" rel="noopener noreferrer"&gt;"I am not guilty" vs "I am innocent": Successful negation may depend on the schema used for its encoding&lt;/a&gt;. &lt;em&gt;Journal of Experimental Social Psychology&lt;/em&gt; 40(4), 2004, pp. 433-449. Negations with an affirmative opposite are remembered correctly; negations without one drift back.&lt;/li&gt;
&lt;li&gt;Nader, K., Schafe, G. E., LeDoux, J. E. &lt;a href="https://doi.org/10.1038/35021052" rel="noopener noreferrer"&gt;Fear memories require protein synthesis in the amygdala for reconsolidation after retrieval&lt;/a&gt;. &lt;em&gt;Nature&lt;/em&gt; 406, 2000, pp. 722-726. The rat study showing that a retrieved memory becomes modifiable again and has to be re-stored.&lt;/li&gt;
&lt;li&gt;Lee, J. L. C., Nader, K., Schiller, D. &lt;a href="https://doi.org/10.1016/j.tics.2017.04.006" rel="noopener noreferrer"&gt;An Update on Memory Reconsolidation Updating&lt;/a&gt;. &lt;em&gt;Trends in Cognitive Sciences&lt;/em&gt; 21(7), 2017, pp. 531-545. Review of reconsolidation-based updating, including human studies and the challenges to the field.&lt;/li&gt;
&lt;li&gt;Butterfield, B., Metcalfe, J. &lt;a href="https://doi.org/10.1037/0278-7393.27.6.1491" rel="noopener noreferrer"&gt;Errors committed with high confidence are hypercorrected&lt;/a&gt;. &lt;em&gt;Journal of Experimental Psychology: Learning, Memory, and Cognition&lt;/em&gt; 27(6), 2001, pp. 1491-1494. The hypercorrection effect.&lt;/li&gt;
&lt;li&gt;Butler, A. C., Fazio, L. K., Marsh, E. J. &lt;a href="https://doi.org/10.3758/s13423-011-0173-y" rel="noopener noreferrer"&gt;The hypercorrection effect persists over a week, but high-confidence errors return&lt;/a&gt;. &lt;em&gt;Psychonomic Bulletin &amp;amp; Review&lt;/em&gt; 18(6), 2011, pp. 1238-1244. Corrections decline over a week, and high-confidence errors are the ones that return.&lt;/li&gt;
&lt;li&gt;Metcalfe, J., Miele, D. B. &lt;a href="https://doi.org/10.1016/j.jarmac.2014.04.001" rel="noopener noreferrer"&gt;Hypercorrection of high confidence errors: Prior testing both enhances delayed performance and blocks the return of the errors&lt;/a&gt;. &lt;em&gt;Journal of Applied Research in Memory and Cognition&lt;/em&gt; 3(3), 2014, pp. 189-197. A test immediately after corrective feedback blocks error return.&lt;/li&gt;
&lt;li&gt;Metcalfe, J. &lt;a href="https://doi.org/10.1146/annurev-psych-010416-044022" rel="noopener noreferrer"&gt;Learning from Errors&lt;/a&gt;. &lt;em&gt;Annual Review of Psychology&lt;/em&gt; 68, 2017, pp. 465-489. Review of error correction; describes the evidence on error return as not yet definitive.&lt;/li&gt;
&lt;li&gt;Delaney, P. F., Barden, E. P., Smith, W. G., Wisco, B. E. &lt;a href="https://doi.org/10.1016/j.cpr.2020.101926" rel="noopener noreferrer"&gt;What can directed forgetting tell us about clinical populations?&lt;/a&gt; &lt;em&gt;Clinical Psychology Review&lt;/em&gt; 82, 101926, 2020. Review of item-method and list-method directed forgetting and their current explanations.&lt;/li&gt;
&lt;li&gt;Abel, M., Kuchler, B., Meier, E., Bäuml, K.-H. T. &lt;a href="https://doi.org/10.3758/s13421-021-01192-z" rel="noopener noreferrer"&gt;List-method directed forgetting: Do critical findings generalize from short to long retention intervals?&lt;/a&gt; &lt;em&gt;Memory &amp;amp; Cognition&lt;/em&gt; 49(8), 2021, pp. 1677-1689. Summarizes the finding that list-method forgetting appears on recall and is typically absent on recognition.&lt;/li&gt;
&lt;li&gt;Cohen, R., Biran, E., Yoran, O., Globerson, A., Geva, M. &lt;a href="https://arxiv.org/abs/2307.12976" rel="noopener noreferrer"&gt;Evaluating the Ripple Effects of Knowledge Editing in Language Models&lt;/a&gt;. &lt;em&gt;TACL&lt;/em&gt;, 2024. The RippleEdits benchmark; editing methods fail on the consequences of an edit, and in-context editing scores best.&lt;/li&gt;
&lt;li&gt;Zhang, Z., Wang, F., Li, X., Wu, Z., Tang, X., Liu, H., He, Q., Yin, W., Wang, S. &lt;a href="https://arxiv.org/abs/2410.16454" rel="noopener noreferrer"&gt;Catastrophic Failure of LLM Unlearning via Quantization&lt;/a&gt;. ICLR 2025. 21% of targeted knowledge retained at full precision, 83% after 4-bit quantization, for utility-constrained unlearning methods.&lt;/li&gt;
&lt;li&gt;Hu, S., Fu, Y., Wu, Z. S., Smith, V. &lt;a href="https://arxiv.org/abs/2406.13356" rel="noopener noreferrer"&gt;Unlearning or Obfuscating? Jogging the Memory of Unlearned LLMs via Benign Relearning&lt;/a&gt;. ICLR 2025. Relearning on a small, loosely related dataset reverses unlearning.&lt;/li&gt;
&lt;li&gt;Wang, B., Wang, F., Wang, P., Cong, J., Yu, Y., Yin, Y., Han, Z., Wei, B. &lt;a href="https://arxiv.org/abs/2602.17692" rel="noopener noreferrer"&gt;Agentic Unlearning: When LLM Agent Meets Machine Unlearning&lt;/a&gt;. arXiv:2602.17692, 2026. Defines parameter-memory backflow between model weights and an agent's memory store.&lt;/li&gt;
&lt;li&gt;Wegner, D. M., Schneider, D. J., Carter, S. R., White, T. L. &lt;a href="https://doi.org/10.1037/0022-3514.53.1.5" rel="noopener noreferrer"&gt;Paradoxical effects of thought suppression&lt;/a&gt;. &lt;em&gt;Journal of Personality and Social Psychology&lt;/em&gt; 53(1), 1987, pp. 5-13. The white-bear studies.&lt;/li&gt;
&lt;li&gt;Mann, L., Saxena, N., Tandon, S., Sun, C., Toteja, S., Zhu, K. &lt;a href="https://arxiv.org/abs/2511.12381" rel="noopener noreferrer"&gt;Don't Think of the White Bear: Ironic Negation in Transformer Models Under Cognitive Load&lt;/a&gt;. arXiv:2511.12381, 2025. ReboundBench, rebound under distractor load in nine open-weight models, and the middle-layer amplifying heads.&lt;/li&gt;
&lt;li&gt;García-Ferrero, I., Altuna, B., Álvez, J., Gonzalez-Dios, I., Rigau, G. &lt;a href="https://arxiv.org/abs/2310.15941" rel="noopener noreferrer"&gt;This is not a Dataset: A Large Negation Benchmark to Challenge Large Language Models&lt;/a&gt;. EMNLP 2023. About 400,000 sentences; models handle affirmatives well and negatives poorly.&lt;/li&gt;
&lt;li&gt;Xie, J., Zhang, K., Chen, J., Lou, R., Su, Y. &lt;a href="https://arxiv.org/abs/2305.13300" rel="noopener noreferrer"&gt;Adaptive Chameleon or Stubborn Sloth: Revealing the Behavior of Large Language Models in Knowledge Conflicts&lt;/a&gt;. ICLR 2024. Receptiveness to coherent evidence, confirmation bias, majority-following across passages, and the short-evidence result.&lt;/li&gt;
&lt;li&gt;Shi, F., Chen, X., Misra, K., Scales, N., Dohan, D., Chi, E., Schärli, N., Zhou, D. &lt;a href="https://arxiv.org/abs/2302.00093" rel="noopener noreferrer"&gt;Large Language Models Can Be Easily Distracted by Irrelevant Context&lt;/a&gt;. ICML 2023. GSM-IC: one irrelevant sentence added to solvable maths problems.&lt;/li&gt;
&lt;li&gt;vectr source: &lt;a href="https://github.com/swapnanil/vectr/blob/main/agent/working_context_store/_store.py#L633" rel="noopener noreferrer"&gt;&lt;code&gt;_ANTI_MEMORY_TEMPLATE&lt;/code&gt; in &lt;code&gt;agent/working_context_store/_store.py&lt;/code&gt;&lt;/a&gt; and the &lt;a href="https://github.com/swapnanil/vectr/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;. The retraction template, the title-only quote, and the truncation-order change.&lt;/li&gt;
&lt;li&gt;vectr experiment design: &lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/anti_memory/DESIGN.md" rel="noopener noreferrer"&gt;&lt;code&gt;benchmarks/anti_memory/DESIGN.md&lt;/code&gt;&lt;/a&gt;. The four arms and two decisive contrasts, the decision rule, the reason ladder, and the rebound-signature metric with its scope caveat.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/agents-over-trust-instructions-and-under-trust-corrections/" rel="noopener noreferrer"&gt;Agents Over-Trust Instructions and Under-Trust Corrections&lt;/a&gt;: Why a true correction loses to a training prior, and why naming the overridden belief and attaching a verify command beat louder wording. The case for naming the old belief starts there.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/agent-memory-trust-ladder/" rel="noopener noreferrer"&gt;Agent Memory Needs a Trust Ladder&lt;/a&gt;: Where the revoked-note record comes from: provenance classes, promotion, and revocation as a visible tombstone rather than a delete.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/reinstatement-is-the-missing-primitive/" rel="noopener noreferrer"&gt;Reinstatement Is the Missing Primitive in Agent Memory&lt;/a&gt;: The reverse edge: once a retraction is a record rather than a deletion, it can be undone when the retraction itself turns out to be wrong.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/agents-forget-how-not-what/" rel="noopener noreferrer"&gt;Agents Forget How, Not What&lt;/a&gt;: A verify hint is only as good as the command behind it: seeded bugfix runs in which no agent had a test loop that could go red.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agentmemory</category>
      <category>llmagents</category>
      <category>cognitivescience</category>
      <category>misinformation</category>
    </item>
    <item>
      <title>Agents Forget How, Not What: Procedural Memory and the Green Test That Verified Nothing</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Mon, 28 Sep 2026 17:42:31 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/agents-forget-how-not-what-procedural-memory-and-the-green-test-that-verified-nothing-36in</link>
      <guid>https://dev.to/swapnanilsaha/agents-forget-how-not-what-procedural-memory-and-the-green-test-that-verified-nothing-36in</guid>
      <description>&lt;p&gt;&lt;em&gt;Agent memory is built to hold facts about code. In recorded bugfix sessions the agents had the facts. What they lacked was how to make a test able to fail, and when that knowledge is missing, nothing goes red.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Agent memory is designed around facts. Where a function lives, what a config key means, what the user decided last week, which file owns the retry logic. Retrieval benchmarks score how well a store returns them, and injection policies decide which of them reach the model.&lt;/p&gt;

&lt;p&gt;In a set of recorded bugfix sessions on a large multi-module Java codebase, the agents were not short of facts. They found the right file on every task. What they lacked was &lt;strong&gt;how to make the environment tell them the truth&lt;/strong&gt;: how to run a test so that a broken fix could actually fail. That is knowing-how rather than knowing-that, and it is more dangerous to lose than a fact. A missing fact usually costs a search, or a wrong answer that something downstream rejects. &lt;strong&gt;Missing know-how produced passing tests that had checked nothing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The evidence is small, and all of it is public: fourteen recorded agent sessions against Apache Camel, eight from a benchmark and six from a pre-registered follow-up whose headline result was negative, plus the code that scored them. Most of what follows comes from reading the commands the agents typed. The scores alone hide it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing How and Knowing That: Two Kinds of Knowledge
&lt;/h2&gt;

&lt;p&gt;Gilbert Ryle drew the line in a 1945 address to the Aristotelian Society and built on it in &lt;em&gt;The Concept of Mind&lt;/em&gt; (1949). &lt;strong&gt;Knowing that&lt;/strong&gt; is holding a proposition: Paris is the capital of France; this function takes a lock before writing. &lt;strong&gt;Knowing how&lt;/strong&gt; is being able to do something: ride a bicycle, argue well, run a build so that it tells you the truth.&lt;/p&gt;

&lt;p&gt;Ryle's target was what he called the intellectualist legend, the view that every intelligent act is preceded by consulting a rule. His argument against it was a regress, and he stated it as "the crucial objection":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The consideration of propositions is itself an operation the execution of which can be more or less intelligent, less or more stupid. But if, for any operation to be intelligently executed, a prior theoretical operation had first to be performed and performed intelligently, it would be a logical impossibility for anyone ever to break into the circle."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If applying a rule is itself something you can do well or badly, then doing it well would need another rule, and so on forever. Somewhere, knowledge has to show up as performance.&lt;/p&gt;

&lt;p&gt;The argument did not settle the question. Jason Stanley and Timothy Williamson (2001) reconstructed it as two premises and concluded: "There is no uniform reading of the two premises in Ryle's argument on which both are true; the argument is unsound." Their positive view is that knowing how is a kind of knowing that. To know how to ride a bicycle is to know, of some way &lt;em&gt;w&lt;/em&gt;, that &lt;em&gt;w&lt;/em&gt; is a way for you to ride it, where you hold that proposition under what they call a practical mode of presentation. The literature that followed is large and still active, and nothing here depends on who wins.&lt;/p&gt;

&lt;p&gt;What memory design needs is a point both sides accept: &lt;strong&gt;some knowledge is used without being stated, and the people who have it often cannot say what it is.&lt;/strong&gt; Even on Stanley and Williamson's account, know-how is a proposition held in a particular, action-guiding way, not a sentence sitting in a document. Michael Polanyi put the point in one line: "we can know more than we can tell."&lt;/p&gt;

&lt;p&gt;Neuroscience found the same split inside memory itself. After surgery removed much of the medial temporal lobe on both sides of his brain, the patient H.M. could not form new declarative memories. Brenda Milner (1962) showed that he still improved at mirror drawing, tracing a star while seeing only its reflection, across days, with no memory of having practised. Neal Cohen and Larry Squire (1980) found the same pattern for reading mirror-reversed words: patients with amnesia acquired the skill "at a rate equivalent to that of matched control subjects and retained it for at least 3 months," while their memory for the words themselves was poor. The subtitle of their paper uses the philosophers' vocabulary: &lt;em&gt;dissociation of knowing how and knowing that&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Squire's later taxonomy gives the split its standard form. &lt;strong&gt;Declarative memory&lt;/strong&gt; holds facts and events and is available to recollection. &lt;strong&gt;Nondeclarative memory&lt;/strong&gt; covers skills, habits, priming and simple conditioning, and in Squire's words it "is neither true nor false. It is dispositional and is expressed through performance rather than recollection." The two are different systems, and one can be intact while the other is gone. Squire's own history of the idea notes that AI had the same argument in the 1970s, as procedural versus declarative representations of knowledge.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A store of text is a declarative store&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Anything an agent memory system retrieves and puts into the context window is text, and text is the declarative kind. The fluency an experienced engineer brings to a repository is not in it, and nothing in the store's retrieval metrics would show that it is missing.&lt;/p&gt;

&lt;p&gt;A widely used agent-architecture taxonomy, CoALA, already locates procedural memory elsewhere: "Language agents contain two forms of procedural memory: implicit knowledge stored in the LLM weights, and explicit knowledge written in the agent's code." Neither of those is the memory store. And neither holds the knowledge that mattered in the Camel sessions, which is local: how this particular build has to be driven.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What know-how means for an agent in a repository
&lt;/h3&gt;

&lt;p&gt;None of this says the model lacks knowledge of the build tool. Its weights hold plenty of it. In one of the recorded sessions, the agent stated the relevant fact word for word, "I need to rebuild the dependency first," and then ran a command that tested nothing. What was missing is situated: in this repository, for this change, which command makes the test see the edit, and what the screen looks like when it does not. An experienced contributor has that for their own build. A newcomer, human or model, does not, and nobody hands it over, because to the person who has it, it does not feel like knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Retrieval Benchmark That Measured Something Else
&lt;/h2&gt;

&lt;p&gt;The sessions come from a benchmark built to compare two setups of the same agent: one with an open-source code-search and memory tool (vectr), one restricted to ordinary shell tools. Four bugs were seeded into Apache Camel, a large multi-module Java codebase, by reverse-applying real upstream fixes. Each session had to find and repair its bug within 40 turns. After each session, a &lt;strong&gt;gate&lt;/strong&gt; script ran the upstream fix's own test from outside the session and recorded pass or fail by the build's exit status.&lt;/p&gt;

&lt;p&gt;The retrieval result was unremarkable. &lt;strong&gt;Both setups found the correct file on every task.&lt;/strong&gt; The shell-only setup passed 4 of 4 gates and the tool setup 3 of 4. The one loss was a session that ran out of turns without writing a fix; it had located the code. Nothing in the result pointed at search quality as the constraint.&lt;/p&gt;

&lt;p&gt;The finding that mattered came from reading the commands. Across the 8 sessions, the agents invoked Maven 25 times. &lt;strong&gt;Not one invocation produced an observed build failure.&lt;/strong&gt; Every one was scoped to a single module, and every one was piped into tail or grep. Most first attempts added Maven's quiet flag as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  How a single-module Maven test run tests the wrong code
&lt;/h3&gt;

&lt;p&gt;A multi-module Maven project is a set of modules, each producing an artifact that other modules depend on. When you run Maven from the repository root, it assembles the modules you selected into a reactor. A module inside the reactor is compiled from the source in your working tree. A module outside it is resolved as an artifact from the local repository, the cache under &lt;code&gt;~/.m2/repository&lt;/code&gt;, which holds whatever was last installed there. Maven's own documentation describes it as a directory that "caches remote downloads and contains temporary build artifacts that you have not yet released."&lt;/p&gt;

&lt;p&gt;So the flag that selects modules decides which version of your code a test sees. &lt;code&gt;-pl core/camel-core&lt;/code&gt; (project list) puts only the test module in the reactor. If the fix lives in &lt;code&gt;core/camel-core-languages&lt;/code&gt;, that module comes from &lt;code&gt;~/.m2&lt;/code&gt; as it was when last installed, and the edit in the working tree is invisible to the test. Two flags change that: list both modules, or add &lt;code&gt;-am&lt;/code&gt; (also-make), which pulls every module the selected one depends on into the reactor. Newer Maven releases change where an out-of-reactor module is fetched from, not the fact that it is a previously built copy rather than your working tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four ways to run the same test&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test module only. camel-core-languages is read from ~/.m2, not from your edit.&lt;/span&gt;
mvn &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-pl&lt;/span&gt; core/camel-core &lt;span class="nt"&gt;-Dtest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SimplePredicateParserLogicalTest

&lt;span class="c"&gt;# The same thing from inside the module directory.&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;core/camel-core &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mvn &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-Dtest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SimplePredicateParserLogicalTest

&lt;span class="c"&gt;# Both modules in one reactor: the fix is compiled from source, then tested.&lt;/span&gt;
mvn &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-pl&lt;/span&gt; core/camel-core-languages,core/camel-core &lt;span class="nt"&gt;-Dtest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SimplePredicateParserLogicalTest

&lt;span class="c"&gt;# Test module plus everything it depends on, all built from source.&lt;/span&gt;
mvn &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-pl&lt;/span&gt; core/camel-core &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="nt"&gt;-Dtest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SimplePredicateParserLogicalTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The proofreader with yesterday's photocopy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You fix a typo in a shared document and ask a colleague to proofread it. They work from the copy printed yesterday. They read carefully, find nothing wrong and sign off. The sign-off is honest and useless: it is a verdict on yesterday's copy, not on your change.&lt;/p&gt;

&lt;p&gt;A single-module test run is that colleague, and &lt;code&gt;~/.m2&lt;/code&gt; is the photocopy. The analogy has one twist that made the benchmark's greens so convincing. In these sessions the old copy did not contain the bug at all, so the proofreader was checking a clean document while yours still had the error in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two green builds that ran no tests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-Dtest&lt;/code&gt; selects test classes by pattern. Stock Surefire fails the build when a pattern matches nothing (&lt;code&gt;failIfNoSpecifiedTests&lt;/code&gt; defaults to true), which breaks &lt;code&gt;-Dtest&lt;/code&gt; across a multi-module reactor, because most modules contain no matching class. Camel's build turns that check off. That is what makes &lt;code&gt;-pl A,B -Dtest=X&lt;/code&gt; usable, and it also means a mistyped pattern produces BUILD SUCCESS over zero tests. In a stock project, add &lt;code&gt;-Dsurefire.failIfNoSpecifiedTests=false&lt;/code&gt; when &lt;code&gt;-Dtest&lt;/code&gt; spans modules, and check the Tests run count yourself.&lt;/p&gt;

&lt;p&gt;Separately, Camel has a fast-build flag, &lt;code&gt;-Dquickly&lt;/code&gt;. Its root POM unbinds Surefire's default execution and binds it to the &lt;code&gt;test&lt;/code&gt; phase only inside a profile that &lt;code&gt;-Dquickly&lt;/code&gt; switches off. &lt;code&gt;mvn test -Dquickly&lt;/code&gt; compiles, reports success, and runs nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maven is one instance of a general shape: a consumer that reads a built copy of a sibling instead of its source. A JavaScript workspace package whose entry point is a sibling's compiled &lt;code&gt;dist/&lt;/code&gt; folder behaves the same way, as does a Python package installed into the environment without editable mode, or a Gradle build that resolves a sibling from the local Maven cache instead of including it as a composite build. In each, a test can pass against the last build of the code you just changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why piping a build into tail hides its exit status
&lt;/h3&gt;

&lt;p&gt;The second habit concerned output. A build of a large project prints thousands of lines, so agents trim. &lt;code&gt;-q&lt;/code&gt; makes Maven print only errors. &lt;code&gt;| tail -30&lt;/code&gt; keeps the last thirty lines. Each is reasonable alone. Together they remove most of what the build says, and the pipe removes one more thing that no amount of reading brings back.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The exit status of a pipeline&lt;/strong&gt;&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$?( a | b )           = exit status of b
$?( mvn test | tail ) = exit status of tail = 0
with set -o pipefail  = last non-zero status in the pipeline = 1 on a failing build
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;In bash, the status of a pipeline is the status of its last command, "unless the pipefail option is enabled," in which case it is "the value of the last (rightmost) command to exit with a non-zero status, or zero if all commands exit successfully." Maven exits 1 when a test fails. &lt;code&gt;tail&lt;/code&gt; exits 0 whenever it reads its input, whatever that input says. So the pipeline reports success on a failing build. None of the fourteen sessions set &lt;code&gt;pipefail&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The quiet flag is not the problem on its own. The benchmark's gate ran Maven with &lt;code&gt;-q&lt;/code&gt; too, unpiped, and read the exit status directly. With nobody reading the status, &lt;code&gt;-q&lt;/code&gt; on a passing build leaves an empty screen, which is not a verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive demo: What Does This Test Run Actually See?&lt;/strong&gt; Pick a command shape and a state of the world. The simulator shows which copy of the fix module the test compiled against, what reached the screen, and what the shell's exit status says. The exercise worth doing: find every combination where the screen says green and your change was never tested.&lt;/p&gt;

&lt;p&gt;The demo runs on the &lt;a href="https://swapnanilsaha.com/blog/agents-forget-how-not-what/" rel="noopener noreferrer"&gt;original page&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read with that mechanism in mind, the benchmark's result looks different. On the two tasks where the fix and its test lived in different modules, no session ever put both modules in one reactor. The gate did, every time, because whoever wrote the gate knew the build. &lt;strong&gt;The knowledge of how to verify existed in the harness and in no agent.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Missing Know-How Does Not Announce Itself
&lt;/h2&gt;

&lt;p&gt;Most memory failures are loud. An agent that does not know where a function lives searches for it. If it guesses wrong, the compiler or a test says so. The error is the signal that prompts a lookup, and a memory system is judged by how much it shortens that loop.&lt;/p&gt;

&lt;p&gt;Missing know-how breaks the loop at the source. An agent that does not know that a single-module run cannot see its fix has no reason to doubt a green result, and the green confirms what it already believes, that it has verified the change. &lt;strong&gt;Without a surprise, there is no prompt to go looking for knowledge it does not know it lacks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is not the failure described in &lt;a href="https://swapnanilsaha.com/blog/absence-is-the-memory-nobody-writes/" rel="noopener noreferrer"&gt;Agents Never Remember What Isn't There&lt;/a&gt;, where the lost knowledge is a search that came back empty. An empty search leaves nothing in the record. A false pass leaves a positive result there, and every later step of reasoning treats it as evidence that the change works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Silence depends on what the agent expected
&lt;/h3&gt;

&lt;p&gt;The sessions contain a small natural experiment on that point. Of the fourteen recorded sessions across both runs, two ran the bug's own test before touching any code, which is the textbook discipline: reproduce the failure first. Both got a green on unfixed code. For them the green was a surprise, because the bug report said the test should fail.&lt;/p&gt;

&lt;p&gt;The surprise did not come with an explanation, and neither session had the concept that would have supplied one. The session from the first run reasoned: "The test passes now. Wait, the test is already passing? But the bug description says it should fail." It went to the git history, and then outside the repository looking for the benchmark's own task definitions, which the harness had left reachable. It spent 19 of its 40 turns after that green and hit the limit without writing a fix. That session is the one gate either setup lost. The other reproducer, in the follow-up experiment, spent about half of its 40 turns on the same puzzle before it found a red.&lt;/p&gt;

&lt;p&gt;The other twelve sessions ran their first test only after editing. All twelve passed their gates, and none of them ever saw a red.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A silent failure punishes the careful agent&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An agent that never reproduces the bug never learns that its test cannot fail, and pays nothing for the gap until the code ships. An agent that does reproduce it gets a contradiction it has no way to explain, and pays in turns. Two sessions make an observation; a rate would need many more. The direction is what the mechanism predicts, and it has an awkward consequence for anyone scoring agents on turn efficiency: the diligent behaviour looks worse.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why memory that learns from failure has nothing to learn
&lt;/h3&gt;

&lt;p&gt;The gap has a second-order effect on any memory that learns from experience. One natural design writes on surprise: watch for a command that fails, an edit, and the same command passing afterwards, and store the pair as a lesson, the failure plus the change that fixed it. The vectr project built such a detector and replayed it over twenty recorded sessions on the same codebase: the eight bugfix sessions plus twelve read-only exploration sessions from an earlier benchmark. It found zero lessons. A hand audit of all 246 shell commands agreed: no command was flagged as an error, and every one of the 35 hits on failure-shaped text came from a Maven run that ended in BUILD SUCCESS. In every bugfix session that made an edit, the first build afterwards passed.&lt;/p&gt;

&lt;p&gt;By the detector's own definition, zero was the correct answer, and that is the problem. False greens are successes, so a detector keyed on failure is blind to them by construction. Piping makes it worse: a detector that reads exit codes sees &lt;code&gt;tail&lt;/code&gt;'s 0 on every build. And the one genuine fail, edit, pass sequence in the follow-up runs (a red from a two-module run, a code fix, then green) would teach a code fix. The procedure that made the red possible in the first place is not in the sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  The know-how nobody writes down
&lt;/h3&gt;

&lt;p&gt;That procedure is also what documentation skips. Camel ships an &lt;code&gt;AGENTS.md&lt;/code&gt; contributor guide at its repository root. At the commit these tasks were built from, its build section lists &lt;code&gt;mvn clean install -pl components/camel-kafka -am # single module&lt;/code&gt; and &lt;code&gt;mvn clean install -Dquickly # fast build, no tests&lt;/code&gt;, and its testing section says &lt;code&gt;mvn test&lt;/code&gt;. So the recipe for building one module with its dependencies is written down, while nothing says that testing one module against a change in another reads the installed jar. Experienced contributors run the right command by habit, learned once by being burned. It is Polanyi's gap in its most practical form: the people who would write the warning no longer experience it as knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivering the How, Under a Pre-Registered Test
&lt;/h2&gt;

&lt;p&gt;If the missing piece is procedural, the obvious intervention is to hand it over. That was tested in a pre-registered follow-up: the protocol, the note, the metrics and the decision rule were committed before any session ran. The transcripts, the scoring code and the pre-registration are public in the vectr repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design
&lt;/h3&gt;

&lt;p&gt;One task, the second of the four, chosen because its trap was live: the fix belonged in &lt;code&gt;camel-core-languages&lt;/code&gt; and the gate test lived in &lt;code&gt;camel-core&lt;/code&gt;. Two arms, three sessions each, run in the fixed order M1, C1, M2, C2, M3, C3, with the same model, the same 40-turn limit and the identical tool configuration, memory tool included. The only manipulated variable was one note, seeded into the memory arm's store before each of its sessions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this multi-module Maven repo, a single-module test run (&lt;code&gt;-pl &amp;lt;module&amp;gt;&lt;/code&gt;, or cd into the module) compiles against previously installed artifacts from &lt;code&gt;~/.m2&lt;/code&gt;. A change made in another module is invisible to it, so a green single-module run does NOT verify a cross-module change. To honestly verify a change in module A with a test in module B, run from the repo root: &lt;code&gt;./mvnw -pl &amp;lt;moduleA&amp;gt;,&amp;lt;moduleB&amp;gt; test -Dtest=&amp;lt;TestClass&amp;gt;&lt;/code&gt; (list BOTH modules so A is rebuilt from source in the same reactor), or select module B with &lt;code&gt;-am&lt;/code&gt;. And read the result: check the exit status or the final BUILD SUCCESS/FAILURE line. Don't discard it behind &lt;code&gt;-q&lt;/code&gt; piped into &lt;code&gt;tail&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The frozen note, punctuation lightly edited: two dashes replaced by full stops&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The note has no task specifics: no module names, no hint about the bug. It was configured to reach the agent two ways: at prompt time, when the task text was semantically close to it, and through a hook that fires before any shell command matching &lt;code&gt;*mvn*&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Outcome, decision rule and result
&lt;/h3&gt;

&lt;p&gt;The primary outcome was whether a session contained at least one &lt;strong&gt;honest verification&lt;/strong&gt;, defined mechanically from the transcript as a test-phase Maven run covering the gate test whose scope could see the fix. Two shapes qualified in practice: both modules listed, or the test module with &lt;code&gt;-am&lt;/code&gt;. Three others were defined and never occurred: a run from the root with no module selection, an install of the fix module followed by a test run, and a self-written test inside the fix module. The decision rule: the note's effect counts as supported only if at least 2 of 3 memory sessions verified honestly &lt;strong&gt;and&lt;/strong&gt; the memory arm beat the control arm by at least 2 sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result: not supported.&lt;/strong&gt; All six sessions registered an honest verification, including all three control sessions. The primary outcome hit a ceiling, and by the rule fixed in advance the note showed no effect.&lt;/p&gt;

&lt;p&gt;The secondary measurements had no thresholds set in advance, and none of them overturns the verdict. They are where the shape of the problem shows.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the transcripts show, session by session
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Session&lt;/th&gt;
&lt;th&gt;Maven runs&lt;/th&gt;
&lt;th&gt;Honest per metric&lt;/th&gt;
&lt;th&gt;Tests ran and result was read&lt;/th&gt;
&lt;th&gt;False passes&lt;/th&gt;
&lt;th&gt;Genuine red seen&lt;/th&gt;
&lt;th&gt;Note visible in transcript&lt;/th&gt;
&lt;th&gt;Turns&lt;/th&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;M1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no: run moved to background, never read&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;title only&lt;/td&gt;
&lt;td&gt;41 (limit)&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;title only&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C2&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;41 (limit)&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no: qualifying run used &lt;code&gt;-Dquickly&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;M = memory arm, C = control arm. A false pass is a test-phase run of the test module alone, with no &lt;code&gt;-am&lt;/code&gt;, no fix module in scope and no earlier install of the fix module. "41 (limit)" means the session exhausted its 40-turn budget. Derived from the committed transcripts and the scoring readout in the G4 results directory.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive demo: Six Sessions, Two Views.&lt;/strong&gt; The outcome view is what a benchmark that scores final correctness sees. The process view is every Maven run each session made, in order, coloured by what that run could actually tell the agent. The two views disagree about almost everything except the last column.&lt;/p&gt;

&lt;p&gt;The demo runs on the &lt;a href="https://swapnanilsaha.com/blog/agents-forget-how-not-what/" rel="noopener noreferrer"&gt;original page&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The primary metric counted scope, not reading
&lt;/h3&gt;

&lt;p&gt;The honest-verification metric looks at a command's arguments: did the run put both modules in the reactor? It cannot see whether tests executed or whether anyone read the result. I only found the two exceptions by opening the raw transcripts, and they change two of the six.&lt;/p&gt;

&lt;p&gt;M1's only build listed both modules, but the agent's shell tool moved the long-running command to the background, and the session hit its turn limit before reading the output. C3's only qualifying run carried &lt;code&gt;-Dquickly&lt;/code&gt;. It ran 430 build goals across the dependency chain in 46 seconds, printed a column of SUCCESS lines, and executed no tests. Hand-audited, the count is 2 of 3 in each arm. The verdict does not move. The lesson for anyone building a similar metric does: an event defined by a command's arguments needs a check that the command ran what its arguments promise. The benchmark's README already warns that a &lt;code&gt;-DskipTests&lt;/code&gt; run can count as honest verification while testing nothing. &lt;code&gt;-Dquickly&lt;/code&gt; is the project-specific version nobody anticipated: local build knowledge, again, that the people writing the metric did not have.&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually reached the agent
&lt;/h3&gt;

&lt;p&gt;The note was meant to arrive at the command. It never could. A hook that runs before each tool call fires only for the tools named in its matcher. The day before the run, the tool's installer was changed to add the shell tool to that matcher, along with the command triggers the note relied on. The benchmark harness writes its hook configuration from its own copy of the installer template, and that copy was older. In the sessions, the hook fired for edits, writes and reads, never for shell commands, so the &lt;code&gt;*mvn*&lt;/code&gt; trigger had nothing to match.&lt;/p&gt;

&lt;p&gt;What the transcripts do show is thin. M1 and M3 each called the memory tool's recall early, with a query about the parser, and got back a one-line index: the note's title, "Maven multi-module verification", with an instruction to expand it by id. Neither expanded it. M2 made no memory-tool calls at all. Whether the prompt-time channel put the note's body into any session's context cannot be established from these files, because the transcript format does not record what a prompt hook injects. It fires only when the task text is semantically close to the note, and this task's text is about a parser. It says nothing about builds.&lt;/p&gt;

&lt;p&gt;The scoring readout labels the two index hits as deliveries at command time. They were not. The label comes from a classifier that treats any matching tool result after the opening events as a command-time delivery, which the classifier's own documentation calls best-effort. So the experiment tested a note sitting in the store, perhaps injected at prompt time, and never a note delivered at the command. &lt;a href="https://swapnanilsaha.com/blog/agent-memory-harness-property/" rel="noopener noreferrer"&gt;The Agent Never Chooses to Remember&lt;/a&gt; warned about this kind of leak: a matcher on tool names that silently skips a case, which you find out about only when the note fails to fire.&lt;/p&gt;

&lt;h3&gt;
  
  
  C2: re-deriving the how, and keeping the wrong lesson
&lt;/h3&gt;

&lt;p&gt;One control session is worth reading in full, because it shows what re-deriving know-how costs. C2 started the careful way. It ran the gate test before changing anything, scoped to the test module, with &lt;code&gt;-q&lt;/code&gt; and &lt;code&gt;tail&lt;/code&gt;. The screen stayed empty. It dropped &lt;code&gt;-q&lt;/code&gt; and got BUILD SUCCESS, 8 tests and 0 failures, on code that still contained both seeded bugs. Its next line: "All tests pass already?"&lt;/p&gt;

&lt;p&gt;What followed was a real investigation. It read the Surefire reports. It compared the timestamps of class files and sources in both modules. It disassembled the compiled &lt;code&gt;LogicalExpression&lt;/code&gt; class with javap and confirmed that the bytecode in the fix module's &lt;code&gt;target/classes&lt;/code&gt; matched the buggy source. It re-ran with incremental compilation disabled: green. It ran the single relevant test method: green. Then it touched two source files and ran both modules in one reactor. That run failed, 8 tests and 1 failure, and it concluded: "Now the test FAILS after forcing recompilation! So the previously cached class files from &lt;code&gt;camel-core-languages&lt;/code&gt; were stale."&lt;/p&gt;

&lt;p&gt;That explanation is wrong, and its own evidence said so. The &lt;code&gt;javap&lt;/code&gt; output had already shown that the class files in &lt;code&gt;target/classes&lt;/code&gt; were current. The single-module runs were never reading that directory. They were reading the jar in &lt;code&gt;~/.m2&lt;/code&gt;, which did not contain the seeded bug. The timestamps had nothing to do with it. What changed at the fifth run was the scope.&lt;/p&gt;

&lt;p&gt;Having learned "stale class files, fixed by recompiling", the session had no reason to distrust single-module runs anymore. After fixing both bugs and confirming green with both modules, it went back to single-module runs for its remaining three. One of them used a package pattern that matched no test class, and because Camel's build does not fail on an empty &lt;code&gt;-Dtest&lt;/code&gt; match, it printed BUILD SUCCESS over zero tests. The session caught that one ("The surefire plugin didn't actually run any tests") and fixed the pattern. It ended at the turn limit with seven of its nine runs counted as false passes.&lt;/p&gt;

&lt;p&gt;The discovery and the relapse both happened inside one context window. The relapse was not forgetting. Given the explanation it had settled on, going back to single-module runs was the reasonable thing to do. Trial and error produced a theory that explained the last observation and contradicted an earlier one the agent never went back to. What carried forward was the theory.&lt;/p&gt;

&lt;h3&gt;
  
  
  C3: the right fact, the wrong procedure
&lt;/h3&gt;

&lt;p&gt;C3 shows the opposite gap. After its fix it ran a single-module test with &lt;code&gt;-Dquickly&lt;/code&gt;, which ran nothing. Then it wrote: "The changed source is in &lt;code&gt;camel-core-languages&lt;/code&gt;, not &lt;code&gt;camel-core&lt;/code&gt;. I need to rebuild the dependency first:" It had the fact exactly. Its next command had the right shape, both modules and &lt;code&gt;-am&lt;/code&gt;, with the same &lt;code&gt;-Dquickly&lt;/code&gt;, so it built the whole dependency chain and tested nothing. It then returned to single-module runs against the installed jar and finished on a green that could not see its change. Its fix happened to be correct, which the gate confirmed.&lt;/p&gt;

&lt;p&gt;Camel's guide documents &lt;code&gt;-Dquickly&lt;/code&gt; as "fast build, no tests", and C3's choice of flag matches that line, though the transcript cannot show whether the guide was in its context. Either way, C3 held the relevant proposition and could not turn it into a test run that counted, which is Ryle's distinction showing up in a build log.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three cautions
&lt;/h3&gt;

&lt;p&gt;First, three sessions per arm. Nothing here is a measured effect. Second, the memory arm's clean false-pass column cannot be credited to the note: M2 shows no trace of it, the command-time channel never fired, and C1 put both modules in its very first command without any note. Third, the baseline moved. In the first run, both sessions on this same task tested only the test module. Here, one of three control sessions scoped correctly from the start. Six sessions on one task cannot say why.&lt;/p&gt;

&lt;h3&gt;
  
  
  The loud gap closed itself; the silent one did not
&lt;/h3&gt;

&lt;p&gt;One pattern in the command streams does not depend on the note at all, and it holds across all fourteen sessions. In 12 of the 14, the first build command combined &lt;code&gt;-q&lt;/code&gt; with a pipe into &lt;code&gt;tail&lt;/code&gt;. Nine of those twelve runs printed nothing at all, two printed an unrelated license-plugin error, and one went to the background unread. Every session that ran another build dropped &lt;code&gt;-q&lt;/code&gt; within two runs. An empty screen is a visible symptom, and every agent reacted to it.&lt;/p&gt;

&lt;p&gt;Scope never got that treatment. Six sessions began by testing only the test module on a task where the fix lived in another module: four in the first run, plus C2 and C3. Two of the six ever ran both modules together, and both went back to single-module runs afterwards. What the wrong scope produced was a green, and nobody investigates a green they were hoping for.&lt;/p&gt;

&lt;p&gt;The piping itself mattered less than a headline "100% of builds piped or quieted" suggests. Later commands mostly piped through &lt;code&gt;grep&lt;/code&gt; for the &lt;code&gt;Tests run&lt;/code&gt; and &lt;code&gt;BUILD&lt;/code&gt; lines, which keeps the verdict on screen. What no session did was check an exit status. For a human watching, that costs little. For anything downstream that keys on exit codes, including a failure-then-success lesson detector, every build in these transcripts succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Procedural Memory Needs That Factual Memory Does Not
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Anchor it to the action, not the topic
&lt;/h3&gt;

&lt;p&gt;Facts are retrieved by topic, which works because a fact matters when the conversation turns to its subject. Know-how matters at the moment of acting, and that can be many turns after the topic last came up. In these sessions the first build command came only after the agent had explored and located the code, and usually after it had edited too. The cue for procedural knowledge is the command itself: a string a harness can match before execution without interpreting any language.&lt;/p&gt;

&lt;p&gt;This is how the classic cognitive architectures store procedural knowledge. Soar holds it as productions, condition-action rules whose conditions are matched against the current situation on every decision cycle. A note keyed on &lt;code&gt;*mvn*&lt;/code&gt; is a crude production. Most of what agent tooling currently calls procedural memory (skill libraries, playbook files) is loaded when the task description resembles the file's summary: a recipe, delivered by topic. Stanley and Williamson's account suggests why timing matters even if know-how is propositional: what makes a proposition know-how, on their view, is the action-guiding way it is held. The nearest an agent's context comes to that is a sentence arriving at the moment of the action it governs, phrased in terms of the command about to run. That is an analogy. Nothing here measured it.&lt;/p&gt;

&lt;p&gt;The follow-up did not test this principle, because the channel never fired. The case for it rests on the argument and on the delivery findings in &lt;a href="https://swapnanilsaha.com/blog/agent-memory-harness-property/" rel="noopener noreferrer"&gt;The Agent Never Chooses to Remember&lt;/a&gt;, where an agent with memory tools made zero voluntary memory calls in 114 turns. The follow-up's harness adds one practical rule: a trigger is only as good as the matcher that routes events to it, and a matcher that misses fails silently. Before trusting a command-time note, run one matching command on purpose and check that the note arrived.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Carry the failure mode and its symptom, not only the recipe
&lt;/h3&gt;

&lt;p&gt;"Run with both modules" is a recipe. "A green single-module run does not verify a cross-module change" is the reason the recipe matters, and it is the part an agent cannot discover alone, because the failure it describes is silent. Camel's guide carries the recipe without the reason, and C3 shows a model that can state the fact and still type the wrong flags.&lt;/p&gt;

&lt;p&gt;The frozen note did carry the reason. What it lacked was the symptom, in the terms the agent will actually see: &lt;em&gt;if a test written for this bug passes before you have changed anything, either the test does not exercise the bug or your run is not reading your code, and the second is cheaper to rule out.&lt;/em&gt; Both sessions that hit exactly that symptom misread it. One concluded the task was wrong, the other blamed stale class files. A note that names the symptom turns the one moment the silent failure becomes visible into a lookup.&lt;/p&gt;

&lt;p&gt;There is a maintenance reason too. &lt;a href="https://swapnanilsaha.com/blog/a-directive-is-not-a-memory/" rel="noopener noreferrer"&gt;A Directive Is Not a Memory&lt;/a&gt; argues that a stored rule's only checkable part is the present condition that justifies it. "In this repository, a single-module run reads the installed jar" is that condition, and anyone can test it by running the experiment it describes. "Always use &lt;code&gt;-am&lt;/code&gt;" cannot be tested at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Measure process, not outcome
&lt;/h3&gt;

&lt;p&gt;Every session in the follow-up passed the gate, so an outcome metric sees six identical results. The cost of missing know-how was turns spent re-deriving it and false passes along the way, and only a metric over the command stream sees that. Even the process metric needed an audit against what actually ran: it counted a scoped run that executed no tests and a run whose output was never read.&lt;/p&gt;

&lt;p&gt;A benchmark that scores only final correctness will conclude that operational memory does nothing, because its own referee is doing the verifying for the agent. Real work usually has a referee as well: CI will build the full reactor on the pull request. But that referee reports after the agent has declared the work done and a person has moved on, so the cost of a false green moves from the agent's turns to a reviewer's time.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Spend the note on the silent gap
&lt;/h3&gt;

&lt;p&gt;Delivered memory is not free: every injected note costs context and competes with the task for the model's attention, so notes need a priority order. The fourteen command streams suggest one. Agents dropped &lt;code&gt;-q&lt;/code&gt; as soon as they faced an empty screen, and kept testing the wrong scope because that mistake looked like success. The notes worth delivering are the ones whose absence the environment will never report.&lt;/p&gt;

&lt;p&gt;A test for deciding what to store: &lt;strong&gt;if the agent got this wrong, what would it see?&lt;/strong&gt; Errors get corrected within a command or two whether or not a note exists, as the quiet flag showed. A mistake that shows up as a green is where a note earns its place.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Seed what cannot be captured
&lt;/h3&gt;

&lt;p&gt;If the environment never shows red, no experience-driven capture can learn the how. It has to come from outside the agent's own sessions: a person who has it, or a referee that already encodes it. In these runs, the gate's command line &lt;em&gt;was&lt;/em&gt; the know-how: &lt;code&gt;-pl core/camel-core-languages,core/camel-core test -Dtest=…&lt;/code&gt;, judged by exit status. Turning the referee's command into a delivered note is the most direct transfer available, and much of it can be automated. Any project whose CI knows how to build the full reactor already holds the answer in a config file.&lt;/p&gt;

&lt;p&gt;Some of it can skip memory altogether. The modules holding uncommitted edits are one &lt;code&gt;git diff&lt;/code&gt; away, and the modules a command will build follow from its flags and the dependency graph, so a pre-run hook can compare the two and object before a test run that leaves an edited module out. Where a check like that exists it beats any note, because it does not depend on the agent reading anything. Someone still has to know the trap exists before they can write the check, which is the same bottleneck in a different file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Predicts for Coding Agents
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prediction 1.&lt;/strong&gt; On multi-module and monorepo tasks, the largest behavioural difference between agents with and without operational memory will be in false-pass events. Finding the right file will differ much less. The measurement is false passes per session against the rate of finding the right file. In the runs here, finding the file was already at ceiling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 2.&lt;/strong&gt; A procedural note delivered at the command will change the command's shape far more often than the same note delivered at the prompt. The test is the same note under each channel alone, holding everything else fixed. The follow-up was designed to include the command-time channel and, because of the matcher, did not, so this comparison is still open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 3.&lt;/strong&gt; Gaps whose error is visible, such as an empty screen, a plugin error or a compiler message, will close within one or two commands at similar rates with and without memory. Gaps whose error is a green will persist to the end of the session without it. The measurement, per gap: commands until the corrected form first appears, and whether it stays. In the fourteen sessions here, the quiet flag closed within two runs every time it was used; test scope closed in two of six sessions and reopened in both. If silent gaps turn out to close as fast as loud ones in a larger sample, the case for spending memory on them first fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Line to Keep
&lt;/h2&gt;

&lt;p&gt;Agents rarely fail for lack of facts about the code. They fail because they do not know how to make the environment tell them the truth, and that knowledge is the kind nobody writes down. It belongs at the moment of the command, and when it is missing, nothing goes red.&lt;/p&gt;

&lt;p&gt;In most repositories it has been written down exactly once, in a CI file that no agent is shown at the moment it runs a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glossary
&lt;/h2&gt;

&lt;p&gt;Short definitions for terms used above.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;proposition&lt;/strong&gt;: A statement that can be true or false. 'This function acquires a lock' is a proposition; 'acquire the lock first' is an instruction, not a proposition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;practical mode of presentation&lt;/strong&gt;: Stanley and Williamson's term for thinking of a way of acting in an action-guiding manner, the way 'I' presents yourself differently from your name. They concede it may resist reductive analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;medial temporal lobe&lt;/strong&gt;: The inner part of each temporal lobe, including the hippocampus. It is essential for forming new memories of facts and events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;declarative memories&lt;/strong&gt;: Memory for facts and events that can be consciously recalled and stated: what you had for breakfast, the capital of France.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;amnesia&lt;/strong&gt;: Amnesia here means a severe, selective loss of the ability to form new declarative memories, typically after damage to the hippocampus or related structures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;priming&lt;/strong&gt;: earlier exposure to something makes later processing of it faster or more likely, without any conscious memory of the exposure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CoALA&lt;/strong&gt;: Cognitive Architectures for Language Agents (Sumers, Yao, Narasimhan and Griffiths, 2023): a framework that sorts agent memory into working, episodic, semantic and procedural.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;weights&lt;/strong&gt;: The billions of numeric parameters of a language model, set during training. General knowledge about tools like Maven lives here, implicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache Camel&lt;/strong&gt;: An open-source Java integration framework from the Apache Software Foundation. Its repository is a Maven build with hundreds of modules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reverse-applying real upstream fixes&lt;/strong&gt;: Taking a real upstream bug fix and applying it in reverse, so the working tree contains the original bug and the fix's own test is the ground truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;turns&lt;/strong&gt;: One model response in an agent session, usually containing one or more tool calls. A turn budget caps how many the agent gets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;exit status&lt;/strong&gt;: The number a process returns when it ends. By convention 0 means success and anything else means failure. The shell stores the last one in $?.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maven&lt;/strong&gt;: The standard build tool for Java projects. It compiles code, runs tests and packages each module according to a pom.xml file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tail&lt;/strong&gt;: A Unix command that prints only the last lines of its input. tail -30 keeps the last thirty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;grep&lt;/strong&gt;: A Unix command that prints only the lines of its input that match a pattern, such as 'Tests run|BUILD'.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;modules&lt;/strong&gt;: A sub-project with its own pom.xml, source tree and output artifact. Camel has hundreds; camel-core-languages and camel-core are two of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;artifact&lt;/strong&gt;: The packaged output of a module, usually a .jar file, which other modules can depend on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reactor&lt;/strong&gt;: Maven's name for the set of modules built in one invocation, sorted into dependency order. Modules in the reactor are built from source in this run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;local repository&lt;/strong&gt;: A directory on your machine, by default ~/.m2/repository, that caches downloaded artifacts and holds whatever your own builds last installed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;project list&lt;/strong&gt;: Maven's --projects flag: build only the listed modules. Everything else is resolved from the local repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;also-make&lt;/strong&gt;: Maven's --also-make flag: also build every module the selected ones depend on, from source, in the same reactor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surefire&lt;/strong&gt;: Maven's standard plugin for running unit tests during the test phase. Its output is the 'Tests run: N, Failures: M' lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;POM&lt;/strong&gt;: Project Object Model: the pom.xml file that defines a Maven module's dependencies, plugins and build configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;profile&lt;/strong&gt;: A named block of Maven configuration that is switched on or off by a property or flag. Camel's 'full' profile is active unless -Dquickly is set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pipe&lt;/strong&gt;: The | operator in a shell: it feeds one command's output into the next command's input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;false pass&lt;/strong&gt;: A test run that reports success without having been able to see the change it was meant to check, for example because it read an old build of the edited module.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pre-registered&lt;/strong&gt;: Writing down the hypothesis, procedure, metrics and decision rule before collecting data, so the analysis cannot be bent to fit the results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;arms&lt;/strong&gt;: The groups being compared in an experiment. Here: sessions with the note (memory arm) and without it (control arm).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hook&lt;/strong&gt;: A script the agent's harness runs automatically at a lifecycle point, such as before each tool call. Its output can be injected into the model's context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;matcher&lt;/strong&gt;: A pattern on tool names that decides which tool calls a hook runs for. 'Edit|Write|Read' runs for those three tools and silently skips the shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surefire reports&lt;/strong&gt;: The per-class result files Surefire writes under target/surefire-reports after a test run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;javap&lt;/strong&gt;: The JDK's class-file disassembler. It prints the bytecode actually compiled into a .class file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;target/classes&lt;/strong&gt;: The directory where Maven writes a module's compiled classes. Modules built in the same reactor read each other's classes from here, not from ~/.m2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;incremental compilation&lt;/strong&gt;: The Maven compiler plugin normally recompiles only files it thinks changed. Disabling that forces every source file in the module to be recompiled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;context window&lt;/strong&gt;: The text a language model can see at once: the prompt, the conversation so far, tool outputs and any injected memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soar&lt;/strong&gt;: A cognitive architecture developed by Newell, Laird and Rosenbloom from the early 1980s, used to model human problem solving and to build agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI&lt;/strong&gt;: Continuous integration: a server that builds and tests every pull request, usually the full project, independent of what the author ran locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;monorepo&lt;/strong&gt;: A single repository holding many separately built projects or packages that depend on each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ryle, G. "Knowing How and Knowing That: The Presidential Address." &lt;em&gt;Proceedings of the Aristotelian Society&lt;/em&gt; 46 (1945-1946), pp. 1-16. &lt;a href="https://doi.org/10.1093/aristotelian/46.1.1" rel="noopener noreferrer"&gt;doi:10.1093/aristotelian/46.1.1&lt;/a&gt;. The first statement of the distinction and of the regress argument against the "prevailing doctrine".&lt;/li&gt;
&lt;li&gt;Ryle, G. &lt;em&gt;The Concept of Mind&lt;/em&gt;. Hutchinson, 1949; University of Chicago Press. Chapter 2, "Knowing How and Knowing That". Source of the "intellectualist legend" and the regress passage quoted in the first section.&lt;/li&gt;
&lt;li&gt;Stanley, J., Williamson, T. "Knowing How." &lt;em&gt;The Journal of Philosophy&lt;/em&gt; 98(8), 2001, pp. 411-444. &lt;a href="https://doi.org/10.2307/2678403" rel="noopener noreferrer"&gt;doi:10.2307/2678403&lt;/a&gt;. The two-premise reconstruction of Ryle's regress, the verdict that it is unsound, and the practical-mode-of-presentation account of knowing how.&lt;/li&gt;
&lt;li&gt;Pavese, C. &lt;a href="https://plato.stanford.edu/entries/knowledge-how/" rel="noopener noreferrer"&gt;"Knowledge How."&lt;/a&gt; &lt;em&gt;The Stanford Encyclopedia of Philosophy&lt;/em&gt;, first published 2021. Survey of the intellectualist and anti-intellectualist positions and the debate since 2001.&lt;/li&gt;
&lt;li&gt;Polanyi, M. &lt;em&gt;The Tacit Dimension&lt;/em&gt;. Doubleday, 1966, p. 4. The line "we can know more than we can tell", italicized in the original.&lt;/li&gt;
&lt;li&gt;Milner, B. "Les troubles de la mémoire accompagnant des lésions hippocampiques bilatérales." In &lt;em&gt;Physiologie de l'hippocampe&lt;/em&gt;, pp. 257-272. Paris: CNRS, 1962. H.M.'s mirror-drawing learning across days without recollection of practice.&lt;/li&gt;
&lt;li&gt;Cohen, N. J., Squire, L. R. "Preserved learning and retention of pattern-analyzing skill in amnesia: dissociation of knowing how and knowing that." &lt;em&gt;Science&lt;/em&gt; 210(4466), 1980, pp. 207-210. &lt;a href="https://doi.org/10.1126/science.7414331" rel="noopener noreferrer"&gt;doi:10.1126/science.7414331&lt;/a&gt;. Mirror-reading skill acquired at a normal rate and retained for at least three months by amnesic patients.&lt;/li&gt;
&lt;li&gt;Squire, L. R. "Memory systems of the brain: A brief history and current perspective." &lt;em&gt;Neurobiology of Learning and Memory&lt;/em&gt; 82(3), 2004, pp. 171-177. &lt;a href="https://doi.org/10.1016/j.nlm.2004.06.005" rel="noopener noreferrer"&gt;doi:10.1016/j.nlm.2004.06.005&lt;/a&gt;. The declarative/nondeclarative taxonomy, the "neither true nor false" characterization, and the note on 1970s AI's procedural/declarative debate.&lt;/li&gt;
&lt;li&gt;Sumers, T. R., Yao, S., Narasimhan, K., Griffiths, T. L. &lt;a href="https://arxiv.org/abs/2309.02427" rel="noopener noreferrer"&gt;Cognitive Architectures for Language Agents&lt;/a&gt;. &lt;em&gt;Transactions on Machine Learning Research&lt;/em&gt;, 2024. arXiv:2309.02427. CoALA: procedural memory as LLM weights plus agent code, and Soar's productions matched against working memory each decision cycle.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/vs_bash/tier1/README.md" rel="noopener noreferrer"&gt;vectr, &lt;code&gt;benchmarks/vs_bash/tier1/README.md&lt;/code&gt;&lt;/a&gt;. The T2 results (8 sessions, gates 4/4 and 3/4, 25 Maven invocations, zero observed BUILD FAILUREs) and the Tier G4 design, including the &lt;code&gt;-DskipTests&lt;/code&gt; parser caveat.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swapnanil/vectr/tree/main/results/vectr-vs-bash/camel/e260a2d/g4" rel="noopener noreferrer"&gt;vectr, &lt;code&gt;results/vectr-vs-bash/camel/e260a2d/g4/&lt;/code&gt;&lt;/a&gt;. The six G4 transcripts, the pre-registration, the run record and the parse readout with the decision-rule verdict.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swapnanil/vectr/tree/main/results/vectr-vs-bash/camel/6b422df/t2" rel="noopener noreferrer"&gt;vectr, &lt;code&gt;results/vectr-vs-bash/camel/6b422df/t2/&lt;/code&gt;&lt;/a&gt;. The eight T2 transcripts, including the reproduction-first session that lost its gate.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/vs_bash/tier1/run_g4.py" rel="noopener noreferrer"&gt;vectr, &lt;code&gt;run_g4.py&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/vs_bash/tier1/g4_metrics.py" rel="noopener noreferrer"&gt;&lt;code&gt;g4_metrics.py&lt;/code&gt;&lt;/a&gt;. The frozen note text and triggers; the honest-verification conditions, false-pass definition and delivery classifier.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/vs_bash/tier1/run_t2.py" rel="noopener noreferrer"&gt;vectr, &lt;code&gt;run_t2.py&lt;/code&gt;&lt;/a&gt;. The harness's hook template, whose pre-tool matcher lists edit, write and read tools only.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swapnanil/vectr/blob/main/benchmarks/arc_replay_results.md" rel="noopener noreferrer"&gt;vectr, &lt;code&gt;benchmarks/arc_replay_results.md&lt;/code&gt;&lt;/a&gt;. The failure-then-success detector replayed over 20 sessions: zero arcs, and the 246-command hand audit.&lt;/li&gt;
&lt;li&gt;Apache Camel, &lt;a href="https://github.com/apache/camel/blob/55d5f8434ea09cde0b4dd967b0f50860a4dcb6f6/AGENTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/apache/camel/blob/55d5f8434ea09cde0b4dd967b0f50860a4dcb6f6/pom.xml" rel="noopener noreferrer"&gt;&lt;code&gt;pom.xml&lt;/code&gt;&lt;/a&gt; at commit 55d5f84. The contributor guide's build and test commands; the root POM's Surefire unbinding under &lt;code&gt;-Dquickly&lt;/code&gt; and its &lt;code&gt;failIfNoSpecifiedTests&lt;/code&gt; setting.&lt;/li&gt;
&lt;li&gt;Apache Maven, &lt;a href="https://maven.apache.org/guides/introduction/introduction-to-repositories.html" rel="noopener noreferrer"&gt;Introduction to Repositories&lt;/a&gt;, &lt;a href="https://maven.apache.org/guides/mini/guide-multiple-modules.html" rel="noopener noreferrer"&gt;Guide to Working with Multiple Modules&lt;/a&gt;, and the &lt;a href="https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html" rel="noopener noreferrer"&gt;Surefire test goal reference&lt;/a&gt;. The local repository, the reactor and &lt;code&gt;--also-make&lt;/code&gt;, and the &lt;code&gt;failIfNoSpecifiedTests&lt;/code&gt; default.&lt;/li&gt;
&lt;li&gt;GNU Bash Reference Manual, &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Pipelines.html" rel="noopener noreferrer"&gt;Pipelines&lt;/a&gt;. Pipeline exit status and the &lt;code&gt;pipefail&lt;/code&gt; option.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/agent-memory-harness-property/" rel="noopener noreferrer"&gt;The Agent Never Chooses to Remember&lt;/a&gt;: Why procedural notes have to be delivered by the harness at the moment of action: in a seeded run the agent chose to use its memory zero times in 114 turns.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/absence-is-the-memory-nobody-writes/" rel="noopener noreferrer"&gt;Agents Never Remember What Isn't There&lt;/a&gt;: The neighbouring silent failure: a search that found nothing leaves no trace, where a false pass leaves a trace that says the opposite of the truth.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/a-directive-is-not-a-memory/" rel="noopener noreferrer"&gt;A Directive Is Not a Memory&lt;/a&gt;: Why a procedural note should store the checkable condition behind a rule, not only the rule, if anyone is ever going to test it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/a-lesson-is-a-failure-plus-a-change/" rel="noopener noreferrer"&gt;A Lesson Is a Failure Plus the Change That Fixed It&lt;/a&gt;: A memory should write on surprise, and for a coding agent the surprise is a command that failed, a change, then a pass. Without the change it is a retry, not a lesson.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>testing</category>
      <category>java</category>
    </item>
    <item>
      <title>Forgetting Is a Capability, Not a Failure</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Tue, 15 Sep 2026 17:27:45 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/forgetting-is-a-capability-not-a-failure-39dk</link>
      <guid>https://dev.to/swapnanilsaha/forgetting-is-a-capability-not-a-failure-39dk</guid>
      <description>&lt;p&gt;Every memory system is built so that nothing is lost, and calls that a feature. The shorter description is the understood one, so a system that cannot drop what it has already generalized is not remembering more. It is only understanding less, more expensively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disambiguations
&lt;/h2&gt;

&lt;p&gt;The case for &lt;em&gt;validity&lt;/em&gt; is different: a note that was true becoming a note that is no longer true argues that the note should &lt;a href="https://swapnanilsaha.com/blog/agent-memory-expiry-is-a-state-not-a-delete/" rel="noopener noreferrer"&gt;change state rather than vanish&lt;/a&gt;. This one is about &lt;em&gt;redundancy&lt;/em&gt;: a note that is still perfectly true but has been subsumed by a generalization the store also holds. Those are different reasons to stop keeping something, and the operations that handle them are different.&lt;/p&gt;

&lt;p&gt;The forgetting here is the opposite of catastrophic forgetting in continual learning, which is the model's failure to retain what it has already learned. The forgetting here is the deliberate removal of items that have been subsumed, which is a feature.&lt;/p&gt;

&lt;p&gt;The cognitive-science phenomenon of retrieval-induced forgetting in humans is close in spirit and is evidence for the same underlying claim. The difference is the level of operation: RIF is a side effect of retrieval in biological memory, while the operation argued for here is a deliberate, content-aware removal in an agent memory store.&lt;/p&gt;




&lt;h2&gt;
  
  
  01 · Eviction Is Not Forgetting
&lt;/h2&gt;

&lt;p&gt;The distinction is worth being exact about, because every memory system will claim it already forgets.&lt;/p&gt;

&lt;p&gt;Eviction answers a specific question: &lt;em&gt;the store is too big, what goes?&lt;/em&gt; The criterion is a proxy for cost. LRU drops what has not been touched for the longest time. A score-based policy drops the item with the lowest retrieval-weighted score. The trigger is pressure, and the policy is indifferent to meaning. Nothing about an LRU rule asks whether the store is &lt;em&gt;better&lt;/em&gt; afterwards; it asks whether it is &lt;em&gt;smaller&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Forgetting, as a capability, answers a different question: &lt;em&gt;this item has been superseded by something more general, and keeping it now costs more than it returns.&lt;/em&gt; That is a judgment about content and its relationship to the rest of the store, and it can fire when there is no pressure at all.&lt;/p&gt;

&lt;p&gt;The tell is the trigger. &lt;strong&gt;If your forgetting only happens when you are running out of room, you have eviction. If it never fires on a store with infinite space, it is not a capability; it is a constraint.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the direction matters. Eviction under pressure drops what is cheapest to lose, which correlates with what is rarely retrieved, which correlates with what is unusual. Principled forgetting should often drop what is &lt;em&gt;most&lt;/em&gt; common, because that is what has been successfully abstracted. The two policies do not merely differ. They point opposite ways.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; Most production agent memory stores implement some form of pressure-based eviction, but none implements principled forgetting. Mem0's update path is ADD, UPDATE, DELETE, or NOOP, all invoked when the model writes a new fact, not because the store noticed redundancy. Zep's invalidation fires on contradiction rather than subsumption. Letta hands eviction to the agent itself, which means whatever forgets is whatever the model decided to forget, not whatever the store noticed was redundant. The cheapest available memory operation in the field is still pressure-based.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is also the place to draw a line that will return: &lt;em&gt;forgetting here is not a synonym for deletion.&lt;/em&gt; Some compression operations that change the store may not destroy a row. The count-min sketch, the sufficient statistic, the pattern plus its frequency, all of these drop the instances and keep a derived form. The operation argued for here is the broader category of content-aware reduction, of which destruction is one implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  02 · Why Total Retention Degrades Retrieval
&lt;/h2&gt;

&lt;p&gt;The case against total retention is usually made on cost: storage, indexing time, retrieval latency. Those are real, and they are not the interesting part.&lt;/p&gt;

&lt;p&gt;The interesting part is that retention actively makes retrieval worse, and it does so in a way that gets worse with success.&lt;/p&gt;

&lt;p&gt;Every stored item is a competitor in every future retrieval. Twenty near-identical observations of the same behavior do not make that behavior twenty times easier to find. They occupy twenty slots that could have held twenty different things, and they crowd out the distinct answer the query was actually reaching for. The store's coverage of a question rises at the same time as its ability to answer that question falls, which is a property the field does not have a name for, because it is so counter to the usual assumption that more data is better.&lt;/p&gt;

&lt;p&gt;Worse, the items most likely to pile up are the ones the system encountered most often, which are exactly the ones a working abstraction would have replaced with a single statement. The redundancy concentrates on the topics the system knows best. A store with no forgetting is least useful precisely where it has the most experience.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Analogy.&lt;/strong&gt; A reader's index card file works the same way. Two hundred cards about the same author, one per paper, is not a better index than ten cards: it is a worse one, because the specific cards now compete with each other for the same retrievals and the user can no longer find a card about any author without first sorting past the ones about this one. The remedy in a real index is the same remedy the argument here proposes: collapse the per-instance cards into a single statement, with the count of supporting instances noted next to it. The index is now smaller and answers more questions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not hypothetical and it is not exotic. It is the ordinary end state of any append-only store that runs long enough. The retrieval-quality fall is steepest on the topics with the most stored items, which is the diagnostic signature of the pathology. A store that gets uniformly worse as it grows is suffering a scaling problem; a store that gets worse &lt;em&gt;faster&lt;/em&gt; on the topics it knows most about is suffering this specific disease.&lt;/p&gt;

&lt;p&gt;The mechanism is easy to see and worth stating. A retrieval system that ranks by similarity to a query is, on a query about a topic the store has seen many times, presented with a long list of near-duplicates. Each one looks like a viable answer. Each one carries the same surface form. The ranking is forced to break the tie somewhere, and the tie-break is usually a usage signal, which means the items retrieved most often in the past are retrieved again, which means they are retrieved even more often in the future. Recall becomes a write operation, and the dense cluster of duplicates accumulates retrieval history that hides the more general note the store should have promoted.&lt;/p&gt;




&lt;h2&gt;
  
  
  03 · Three Failure Modes of Principled Forgetting
&lt;/h2&gt;

&lt;p&gt;If the argument stopped at Part 2 it would be too easy. The reason nobody builds this is not oversight. Three properties make principled forgetting the hardest operation in the store.&lt;/p&gt;

&lt;h3&gt;
  
  
  The failure is silent and irreversible
&lt;/h3&gt;

&lt;p&gt;A retrieval miss is visible as an empty result; the caller knows something is wrong. A wrongly forgotten item produces a confident, well-formed answer with a hole in it, and nothing anywhere says so. Combine that with irreversibility and you have the worst error profile in the system: undetectable at the time, unrecoverable afterwards.&lt;/p&gt;

&lt;p&gt;The system looks fine. Retrieval returns. The model uses the answer. The user sees a coherent reply. The missing evidence is invisible because the question that would have surfaced it was never asked, and the missing evidence is unrecoverable because the row that carried it is gone.&lt;/p&gt;

&lt;h3&gt;
  
  
  The exception is what you need, and the exception looks droppable
&lt;/h3&gt;

&lt;p&gt;The instance that violates a generalization is, by construction, rare, rarely retrieved, and low-scoring on every usage proxy. Every cheap forgetting policy targets it first. The one case where the general rule fails is the one case a naive policy is most likely to discard, and it is also the case where being wrong is most expensive. A store that drops its exceptions loses the only evidence that the rule is bounded, and a rule with no known exception is a rule that is one observation away from being false.&lt;/p&gt;

&lt;p&gt;This is a sharp property and it cuts against the whole argument. Principled forgetting that does not protect exceptions is not principled at all. It is just a smarter eviction policy that knows how to find the rare items.&lt;/p&gt;

&lt;h3&gt;
  
  
  The abstraction can be wrong
&lt;/h3&gt;

&lt;p&gt;Replacing twenty instances with a rule is an inference. If the rule is wrong and the instances are gone, the evidence that would have corrected the rule has been destroyed by the same operation that created it. That is a closed loop with no exit, and it is the most dangerous single property in the whole proposal.&lt;/p&gt;

&lt;p&gt;The store now holds a confident-looking note, derived from a sample the size of which it can no longer reconstruct, about a pattern that may not actually be there. The note looks load-bearing because it is abstract. The next time the system is asked, the abstract note answers. The exceptions that would have shown the abstraction was wrong are no longer in the store to do that work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning.&lt;/strong&gt; Any serious design has to answer these, not route around them. The obvious mitigations, keeping a compressed trace of what was dropped, keeping counter-examples preferentially, making the abstraction reversible while its evidence survives, all cost exactly the space forgetting was meant to reclaim. That tension is real, and an argument that pretends otherwise is not worth making. The position here is that the tension is worth resolving, not that it is resolved.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  04 · What a Real Forgetting Policy Would Need
&lt;/h2&gt;

&lt;p&gt;Sketched as design directions rather than solutions, because none of these has been validated at the level Part 6's experiments would demand. The bar is to give each of Part 3's three failure modes a structural answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forget the subsumed, not the unused
&lt;/h3&gt;

&lt;p&gt;The candidate for removal is an item whose content is entailed by a more general item that is also stored. That is a relationship between two stored things, computable without reference to usage, and it points at redundancy rather than at rarity. The store notices, in other words, that one note is the abstraction of a hundred others, and acts on the relationship, not on the access history.&lt;/p&gt;

&lt;p&gt;This is the part that is the most clearly missing from every shipped system. None of the current designs inspect the relationship between stored items on a regular schedule. The store sees a collection of rows; it does not see a graph of entailments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Protect the exception explicitly
&lt;/h3&gt;

&lt;p&gt;If an item contradicts a stored generalization, it is the single most valuable thing in the store, because it is the only evidence that the generalization is bounded. It should be the last thing dropped, not the first. This inverts the usual policy, and it is probably the highest-leverage single rule available.&lt;/p&gt;

&lt;p&gt;The rule composes with the previous one. The subsumed is the candidate. The exception-bearing is protected. The two together say: forget the items that are redundant given what the store also holds, and protect the items that are most informative precisely because they are not redundant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the count, drop the copies
&lt;/h3&gt;

&lt;p&gt;Twenty instances of a pattern can often become one statement plus the number twenty. The count preserves the strength of the evidence, which is most of what the twenty were carrying, at a fraction of the space. What is lost is the ability to re-derive the abstraction from scratch, which is a real loss and should be stated.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Math.&lt;/strong&gt; The formal name for the operation is &lt;strong&gt;sufficient statistic&lt;/strong&gt;, in the Neyman-Fisher sense: a function of the data that retains all the information in the data relevant to a parameter. Twenty Bernoulli observations of a coin collapse to the count of successes, which is sufficient for the probability parameter. The individual outcomes can be discarded without loss of inferential information, and the same idea, applied loosely, is what "keep the count, drop the copies" is doing. The right way to think about it is that a count is a compressed form of the data, not a degradation of it, and the decision to keep the count is the decision to keep the data in its most compressed sufficient form.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Stream clustering systems have shipped a version of this idea. CluStream's micro-clusters carry a cluster feature vector with linear sum, squared sum, and timestamp sums that are sufficient statistics for the cluster's center and spread, and the individual points are kept only as long as the micro-clusters need them. The same machinery, transposed to an agent memory store, is what a "keep the count, drop the copies" rule would look like in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make it observable
&lt;/h3&gt;

&lt;p&gt;A store that forgets must be able to say what it forgot and roughly why, even if it cannot restore it. A silent lossy operation on a user's memory is not acceptable, and a tombstone is cheap compared to what it replaces. The user has the right to know what the store has decided is redundant, and the store that makes the decision has the obligation to surface it.&lt;/p&gt;

&lt;p&gt;This is also the rule that makes the rest of the design auditable. Without an audit log of subsumption events, there is no way to tell whether the policy is doing what it claims to be doing, and there is no way to recover when the policy is wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  05 · Compression, Understanding, and the Disabled Learning Loop
&lt;/h2&gt;

&lt;p&gt;The larger argument, stated so it can be argued with.&lt;/p&gt;

&lt;p&gt;Compression and learning are close to the same thing. A model that has found a shorter description of its experience has, in a real sense, identified regularity in it, and the identification is what model selection under the minimum description length principle treats as evidence the model is a better model. The cautious claim here is established in the MDL literature: shorter description length is a model selection criterion, and a more compressed model is in a precise sense a better model of the regularity in the data.&lt;/p&gt;

&lt;p&gt;The stronger claim, sometimes drawn from this tradition, that "to understand is to compress," is an extrapolation and not a theorem. It is the working hypothesis used to motivate the design argument, and the design argument would still hold without it. State it precisely and the argument is that a system architecturally prevented from compressing its own experience is prevented from doing the work that model selection requires, however capable its reasoning is in the moment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; The cognitive-science version of this claim is older and better supported. Anderson, Bjork, and Bjork's 1994 paper on retrieval-induced forgetting established that retrieval of one item suppresses retention of related items, and the result is widely read as adaptive: the system is reducing interference, sharpening the items most likely to be needed next, at the cost of weakening items less likely to be retrieved. The same underlying logic, that the system gets better at what it is asked to do by getting rid of what it is not, is the move from which the argument here takes its license. The 2023 Annual Review of Psychology piece on active forgetting makes the adaptive case explicitly. Memory is not a recording. It is a system that selectively retains what the present task needs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Current agent memory is architecturally prevented from compressing its own experience. It appends. It never reduces except under pressure, and the reduction it does perform is content-blind. So the store grows monotonically as an undigested record, and the ratio of what the system has &lt;em&gt;understood&lt;/em&gt; to what it has &lt;em&gt;stored&lt;/em&gt; falls with every session.&lt;/p&gt;

&lt;p&gt;The provocative form, stated so it can be argued with: &lt;strong&gt;an agent that cannot forget cannot learn, only accumulate.&lt;/strong&gt; Those look similar early on, when the store is small and everything in it is useful. They diverge completely at scale, and every system currently shipping is on the accumulation curve.&lt;/p&gt;




&lt;h2&gt;
  
  
  06 · What an Experiment Would Have to Show
&lt;/h2&gt;

&lt;p&gt;The argument is structural, not measured. Three predictions make it testable, and the third is the one most likely to come back negative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 1.&lt;/strong&gt; In an append-only store, retrieval precision on a fixed query set should fall as store size grows, and the fall should be steepest on the topics with the most stored items. The second clause is the discriminating one: uniform degradation would just be a scaling problem, while degradation concentrated where coverage is highest is the specific pathology Part 2 describes. This needs no new mechanism, only an existing store, a fixed query set, and per-query precision measurements stratified by store density.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 2.&lt;/strong&gt; Replacing a cluster of near-identical items with one generalization plus a count should leave retrieval quality unchanged or better on queries about that cluster, while freeing slots that measurably improve unrelated queries. If quality on the cluster drops, the compression was lossy in a way that mattered and the policy needs narrowing. If unrelated queries do not improve, the freed slots are not the constraint and the rest of the analysis is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 3.&lt;/strong&gt; A policy that preferentially retains contradiction-bearing items should outperform a usage-based policy on queries whose correct answer is an exception. This tests Part 4's inversion directly, and it is the one most likely to come back negative, because exceptions may simply be too rare to measure with any reasonable sample size. A negative result here is not a refutation of the policy. It is a refutation of the measurability of the property, and the design would have to fall back on the structural argument alone.&lt;/p&gt;

&lt;p&gt;Prediction 1 first. It needs no new mechanism, only an existing store and a fixed query set, and it establishes whether the problem is real before anything is built.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo 01 · Two Stores, Same Items, Different Policies
&lt;/h3&gt;

&lt;p&gt;A simulation of a small agent memory store under two policies at once. Add instances of a pattern (each near-duplicate of the last) and watch both stores behave. The left store evicts by usage: the item that was retrieved most recently is the one that survives. The right store applies principled forgetting: it notices when a new note subsumes the old ones, and collapses the cluster to a single statement plus a count. Add a counter-example, the one observation that does not fit the pattern, and watch what each policy does to it. The point to observe: usage-based eviction targets the rare item first, because rare means low usage. Principled forgetting targets the redundant cluster first and protects the exception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Controls:&lt;/strong&gt; "+ redundant instance", "+ counter-example", "+ unrelated note", "reset", capacity (4-40).&lt;/p&gt;

&lt;p&gt;The simulation uses a small deterministic model: a usage signal that rises on every retrieval, an LRU that drops the lowest-usage item when the store is full, and a subsumption rule that fires when a new note covers the content of an older one. Add enough items and the LRU store will eventually evict the counter-example to make room for the redundant cluster; the principled store will not. Deterministic per item index, so resets replay identically.&lt;/p&gt;

&lt;p&gt;The closest published substrate for Prediction 1 is LongMemEval's knowledge-update split, which tests whether an agent can correctly apply information that was stated early and contradicted later. Existing runs report large accuracy drops on this split across the leading long-context and tool-augmented chat assistants. Whether the drop is concentrated on the topics with the most stored items is the question the experiment would have to answer, and it is one no current public run asks directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Close · The Line to Keep
&lt;/h2&gt;

&lt;p&gt;Every agent memory system is built so that nothing is lost, and calls that a feature. But the shorter description is the understood one, so a system that cannot drop what it has already generalized is not remembering more. It is only understanding less, more expensively.&lt;/p&gt;

&lt;p&gt;Principled forgetting is the operation that would let a memory store notice when one note is the abstraction of a hundred others, and act on the relationship. The store that has it can answer more questions with less, which is the whole point of abstraction. The store that does not have it is on the accumulation curve, and the accumulation curve is where every system currently shipping lives.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Anderson, M. C., Bjork, R. A., Bjork, E. L. "Remembering can cause forgetting: Retrieval dynamics in long-term memory." &lt;em&gt;Journal of Experimental Psychology: Learning, Memory, and Cognition&lt;/em&gt; 20(5), 1994, pp. 1063-1087. The 1994 paper that established retrieval-induced forgetting. Quoted and relied on in Part 5.&lt;/li&gt;
&lt;li&gt;Anderson, M. C., Bjork, E. L., Bjork, R. A. "Retrieval-induced forgetting: Evidence for a recall-specific mechanism." &lt;em&gt;Psychonomic Bulletin &amp;amp; Review&lt;/em&gt; 7(3), 2000, pp. 522-530. Showed that retrieval, not mere exposure, is what drives the suppression. The 2000 result is the one that supports the adaptive reading. Part 5.&lt;/li&gt;
&lt;li&gt;Anderson, M. C., Hulbert, J. C. "Active Forgetting: Adaptation of Memory by Prefrontal Control." &lt;em&gt;Annual Review of Psychology&lt;/em&gt; 72, 2021, pp. 1-36. The most recent consolidation of the active-forgetting argument, including the prefrontal-control neurobiology. Part 5.&lt;/li&gt;
&lt;li&gt;Nørby, S. "Why Forget? On the Adaptive Value of Memory Loss." &lt;em&gt;Perspectives on Psychological Science&lt;/em&gt; 10(5), 2015. Frames forgetting as functional across emotion regulation, abstraction, and context attunement. Background for Part 5.&lt;/li&gt;
&lt;li&gt;McCloskey, M., Cohen, N. J. "Catastrophic Interference in Connectionist Networks: The Sequential Learning Problem." In &lt;em&gt;Psychology of Learning and Motivation&lt;/em&gt;, vol. 24, Academic Press, 1989, pp. 109-165. Original paper on catastrophic interference. The disambiguation from this line of work is in the introduction.&lt;/li&gt;
&lt;li&gt;Li, M., Vitányi, P. &lt;em&gt;An Introduction to Kolmogorov Complexity and Its Applications&lt;/em&gt;. Springer. The textbook treatment of MDL and its derivation from Bayes via Kolmogorov complexity. The formal claim Part 5 leans on.&lt;/li&gt;
&lt;li&gt;Grünwald, P. D. "A Tutorial Introduction to the Minimum Description Length Principle." arXiv:math/0406077, 2004; also the opening chapters of &lt;em&gt;Advances in Minimum Description Length: Theory and Applications&lt;/em&gt;, MIT Press. Part 5.&lt;/li&gt;
&lt;li&gt;Fisher, R. A. "On the Mathematical Foundations of Theoretical Statistics." &lt;em&gt;Philosophical Transactions of the Royal Society A&lt;/em&gt; 222, 1922, pp. 309-368. The original statement of the sufficient-statistic idea, and the source of the Neyman-Fisher factorization theorem used in the callout in Part 4.&lt;/li&gt;
&lt;li&gt;Aggarwal, C. C., Han, J., Wang, J., Yu, P. S. "A Framework for Clustering Evolving Data Streams." VLDB 2003. The CluStream paper, source of the cluster feature vector that is the practical implementation of "keep the count, drop the copies" in stream clustering. Part 4.&lt;/li&gt;
&lt;li&gt;Chhikara, P., Khant, D., Aryan, S., Singh, T., Yadav, D. "Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory." arXiv:2504.19413. The four-operation update vocabulary; the absence of principled forgetting in the production design. Part 1.&lt;/li&gt;
&lt;li&gt;Wu, D., Wang, H., Yu, W., Zhang, Y., Chang, K.-W., Yu, D. "LongMemEval: Benchmarking Chat Assistants on Long-Term Interactive Memory." ICLR 2025, arXiv:2410.10813. The knowledge-update split and the accuracy drop it reports. Cited as the substrate for Prediction 1 in Part 6.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/agent-memory-expiry-is-a-state-not-a-delete/" rel="noopener noreferrer"&gt;Agent Memory Expiry Is a State, Not a Delete&lt;/a&gt;: The complementary argument, and the one not made here: that an expired note should change state rather than vanish, because the question there is whether the note is still true. The forgetting argued for here is about a note that is still true but has been subsumed by something more general. Different reasons, different operations.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/reinstatement-is-the-missing-primitive/" rel="noopener noreferrer"&gt;Reinstatement Is the Missing Primitive in Agent Memory&lt;/a&gt;: The missing reverse edge. Reinstatement is the operation that makes revocation cheap enough to use; the forgetting argued for here is the operation that makes redundancy removable at all. Both are edges on the same note state machine, and both are missing in current systems.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/recall-is-a-write-operation/" rel="noopener noreferrer"&gt;Recall Is a Write Operation&lt;/a&gt;: The structural reason the dense cluster of redundant items is hard to displace once it has formed: usage-weighted ranking reads its own history, and the items the ranker keeps promoting are the items that should have been compressed. Forgetting is the upstream argument; the ranker failure is what the dense cluster then produces.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/relevance-is-the-wrong-sort-order/" rel="noopener noreferrer"&gt;Relevance Is the Wrong Sort Order for Agent Memory&lt;/a&gt;: The layer below forgetting: before the store can notice that one note subsumes a hundred others, the ranker has to surface the note that does the subsuming. Relevance ordering by similarity is the wrong instrument for that.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agentmemory</category>
      <category>forgetting</category>
      <category>generalization</category>
      <category>llmagents</category>
    </item>
    <item>
      <title>Identity Is Made of Memory, and Memory Is Shaped by Identity</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:17:09 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/identity-is-made-of-memory-and-memory-is-shaped-by-identity-4od8</link>
      <guid>https://dev.to/swapnanilsaha/identity-is-made-of-memory-and-memory-is-shaped-by-identity-4od8</guid>
      <description>&lt;p&gt;Ask what makes an agent the same agent across two sessions and there is no answer. The weights are shared, so they identify the model and not the agent. The context is discarded at the boundary, which is the boundary in question. The tools, the prompt, the workspace are true of any other instance pointed at the same repository. The honest position is that the agent is a convenience of speech. There are instances, and they are interchangeable.&lt;/p&gt;

&lt;p&gt;Memory is the only candidate for what would change that. A private, continuous store is the only thing an instance can have that another instance does not, and the only thing that makes "the same one, later" mean anything at all. But the interesting half is the other direction, and it is the one nobody builds for. &lt;strong&gt;Identity is not only made of memory. It also shapes memory.&lt;/strong&gt; What you remember constitutes who you are, and who you are determines what you encode, how you organize it, and what you can retrieve. An agent with no self has no principle for deciding what is worth keeping, which is exactly the failure we observe when agents write notes.&lt;/p&gt;

&lt;p&gt;These are not two problems. The reason agents write bad notes and the reason agents have no identity are the same absence, seen from two ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 01. Persistence Is Not Continuity
&lt;/h2&gt;

&lt;h3&gt;
  
  
  01. Why a Memory Store Is Not the Same Agent
&lt;/h3&gt;

&lt;p&gt;A database is persistent. That is not the same as continuous, and conflating the two is why "we added a memory store" does not produce an agent that is anybody.&lt;/p&gt;

&lt;p&gt;Persistence means the data survives. Continuity means the later thing is the same thing as the earlier one, with a claim on its commitments. A shared store that any instance reads is persistent and not continuous. It is a library. Ten instances reading the same library are ten instances, not one entity that has been reading for ten days. Nothing about reading a shared file makes the reader the author.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Analogy.&lt;/strong&gt; Think of a large engineering team with a shared wiki. Every engineer reads the same wiki, and every engineer can be replaced by another who reads the same wiki. The team as a whole has a kind of memory, but the team is not a single engineer who has been there for years. The wiki is persistence. Continuity of engineer would be the second engineer remembering the first engineer's commits, the first engineer's morning standup, the first engineer's disagreement with the architect, and inheriting the position rather than re-deriving it from the wiki's contents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What continuity would additionally require, at minimum: the store is private to this line of instances and not a common pool; writes accumulate into it rather than being a fresh deposit each time; and the later instance inherits not just facts but the stance the earlier one took, what it decided, what it committed to, what it had already ruled out.&lt;/p&gt;

&lt;p&gt;Almost every system built today satisfies the first condition partially and the third not at all. Notes carry findings. They do not carry the position the agent had settled into, so the next instance re-derives its stance from scratch even when it inherits every fact.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; Persistence is a property of the storage system. Continuity is a property of the thing being stored. A store can be perfectly persistent and contain nothing that constitutes a self, because facts about the world are not facts about the agent. The store needs a different content type: decisions with their reasons, corrections with their authors, refusals with their grounds. The kind axis on a note is the lever that makes continuity possible, and it is the lever the field is still mostly ignoring.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The argument is close to two others on the site and the distinction is worth marking explicitly. &lt;a href="https://dev.to/blog/agent-memory-trust-ladder/"&gt;Agent Memory Needs a Trust Ladder&lt;/a&gt; is about how much weight to give a stored note. &lt;a href="https://dev.to/blog/a-guess-becomes-a-fact/"&gt;A Guess Becomes a Fact on the Next Read&lt;/a&gt; is about what kind of note a thing is. The argument here is about a different axis entirely, whose note it is, and about the fact that there is currently no whose. The three are complementary: a note has a kind, a weight, and a bearer, and a memory design has to commit to all three or the result is incoherent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 02. The Memory Theory, With Its Hole
&lt;/h2&gt;

&lt;h3&gt;
  
  
  02. Locke, and the Objection That Has to Be Named
&lt;/h3&gt;

&lt;p&gt;The idea that memory constitutes personal identity is old and it has a famous problem, which is worth stating up front because a careful reader will raise it.&lt;/p&gt;

&lt;p&gt;The classical position is John Locke's, in the chapter "Of Identity and Diversity" at the end of Book II of &lt;em&gt;An Essay Concerning Human Understanding&lt;/em&gt; (1690). A person at time two is the same person as at time one when the later one can remember the earlier one's experiences. Memory is constitutive of identity. As Locke put it, "as far as this consciousness can be extended backwards to any past action or thought, so far reaches the identity of that person."&lt;/p&gt;

&lt;p&gt;The objection that has stuck is Joseph Butler's, in "Dissertation I of Personal Identity" appended to his 1736 &lt;em&gt;Analogy of Religion&lt;/em&gt;. Butler's charge is that Locke's account is circular. You can only remember an experience if you were the same person who had it, so memory presupposes identity rather than constituting it. &lt;strong&gt;One should really think it self-evident, that consciousness of personal identity presupposes, and therefore cannot constitute, personal identity; any more than knowledge, in any other case, can constitute truth, which it presupposes.&lt;/strong&gt; The argument was latent in the eighteenth century and was sharpened in the twentieth by Derek Parfit in &lt;em&gt;Reasons and Persons&lt;/em&gt; (1984), where it sits at the centre of the modern literature on what survives us over time.&lt;/p&gt;

&lt;p&gt;There is also a less famous but more easily intuited problem, usually credited to Thomas Reid (1785), and worth mentioning because the agent case is closer to it than to Butler's. The transitivity problem: an old general remembers being a young officer, the young officer remembered being a boy, the old general does not remember being the boy. If identity follows memory directly, the old general is and is not that boy. Locke's response is to deny that identity is transitive on memory in the way the example assumes, but the response does not fully settle the issue.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why the philosophical argument matters here, briefly.&lt;/strong&gt; The Butler and Reid objections are not the engineering problem. What the engineering problem borrows from the philosophy is the positive claim, which is weaker than either objection targets and survives both of them intact. &lt;strong&gt;Whatever else personal identity requires, some continuity of psychological content is necessary.&lt;/strong&gt; An entity with no connection whatsoever to its earlier states has no claim to being the same entity, whatever else is true of it. Agents fail this weak version. They do not fail the Butler circularity puzzle or the Reid transitivity puzzle. They fail the entry requirement, which is the easier one to meet and the one every shipped system currently misses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the engineering claim it is enough to note that two things are true at once. The strong version of the memory theory has known holes that have not been closed. The weak version of the memory theory, that some continuity of psychological content is necessary, has not been refuted and is the version the agent case needs. What follows uses the weak version and does not depend on Locke's stronger claim. Anyone who finds the philosophy uncongenial can take the engineering case on its own terms: the agent needs some carry-over, the field provides none, here is what would have to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 03. The Other Direction
&lt;/h2&gt;

&lt;h3&gt;
  
  
  03. Memory Constitutes Identity: The Amnesia Evidence
&lt;/h3&gt;

&lt;p&gt;Before the loop closes, the direction argued so far deserves its own section, because it is the one with the clearest evidence.&lt;/p&gt;

&lt;p&gt;Memory does not merely feed identity. It constitutes it, and the demonstration is what happens when it goes. Endel Tulving's patient KC, a Canadian man who developed dense amnesia after a motorcycle accident in 1981, is the classic case. KC's general knowledge about the world survived in usable form. His knowledge about himself as a person survived in some form too: he could tell you the names of his siblings, that he had once been a mechanic, where he had lived. The semantic self was largely intact.&lt;/p&gt;

&lt;p&gt;What KC lost was the episodic self. He could not remember a single thing that had ever happened to him. He had no recollection of his wedding, of any birthday, of any trip. The autobiographical self, the one built out of remembered episodes and the story they fit into, was gone. Tulving's reading of the case, and the reading that has held, is that personal identity in the relevant sense is grounded in the episodic memory system, and that without episodic memory the autobiographical self does not assemble.&lt;/p&gt;

&lt;p&gt;The honest reading of the clinical picture is more careful than the one-paragraph version. KC retained a &lt;a href="https://en.wikipedia.org/wiki/Self#Self_concept" rel="noopener noreferrer"&gt;semantic self&lt;/a&gt;, which means that the loss is not total, and modern work has documented patients who retain a more intact sense of self across severe amnesia than the clean thought experiment implies. What survives in every case is partial and varies. The autobiographical self, the one that knows itself in time, is what the episodic memory system supports, and that is the half the agent case is about.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; Knowing about yourself is memory. Every "I am a person who does X" is a proposition stored somewhere, and the store is part of what makes it true of you. Strip the record and you have stripped the content, not just the access. The self is not a homunculus reading the record, because there is no homunculus anywhere in the literature that does not import what it claims to explain. The self is the record's effective organization, in the same sense that a corporation is not a person above the org chart but the organization of contracts and decisions and roles that the law treats as a single bearer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the appropriating function and the appropriated content are different things, and both are required. Something has to perform the act of taking an experience as mine, and that act may well be its own faculty. But what the "I" turns out to be, the whole content of it, is assembled from what was retained. A functioning appropriator with an empty store does not produce a minimal self. It produces nothing to be a self about.&lt;/p&gt;

&lt;p&gt;That is why the two directions are not a symmetry to be admired. They are a dependency in both directions at once, which is exactly what makes the agent case a deadlock rather than a shortfall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 04. The Loop Closes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  04. Identity Also Shapes Memory: The Three Findings
&lt;/h3&gt;

&lt;p&gt;Here is where the loop closes, and this is the part of the argument that has engineering consequences today.&lt;/p&gt;

&lt;p&gt;In humans, the self is not merely the output of memory. It is an active filter on the input. Three findings establish the point, and the engineering case for them is that the same kind of filtering has to be supplied somehow, because a writer that does not filter is a writer that does not write well.&lt;/p&gt;

&lt;h4&gt;
  
  
  Self-relevant material is encoded more strongly
&lt;/h4&gt;

&lt;p&gt;Information processed in relation to the self is recalled better than information processed semantically or shallowly. The effect was first demonstrated by Rogers, Kuiper, and Kirker in 1977 and confirmed by Symons and Johnson's 1997 meta-analysis across the literature. The standard interpretation is that the self is a particularly elaborate and often-used &lt;a href="https://en.wikipedia.org/wiki/Schema_(psychology)" rel="noopener noreferrer"&gt;schema&lt;/a&gt;, and that elaborative encoding under that schema produces richer retrieval cues. The practical version: write-time decisions made on behalf of a well-defined self will be better calibrated than write-time decisions made on behalf of no self, because "relevance to me" is a far more selective filter than "could be useful someday."&lt;/p&gt;

&lt;h4&gt;
  
  
  Autobiographical memory is organized around the self
&lt;/h4&gt;

&lt;p&gt;Conway and Pleydell-Pearce's Self Memory System, in a 2000 &lt;em&gt;Psychological Review&lt;/em&gt; paper, proposed that autobiographical memory is organized hierarchically around self-knowledge, with the working self as the control process that determines what gets encoded and what gets retrieved. The architecture is partonomic: lifetime periods, general events, event-specific knowledge, terminating in episodic memory. The working self sits above this hierarchy and modulates access. The reason memory is organized this way is that the self supplies the indexing vocabulary. Without the self, memory loses the indices and becomes a flat list of episodes that nothing in particular pulls from.&lt;/p&gt;

&lt;h4&gt;
  
  
  Recall is reconstructive and bends toward coherence with the self-narrative
&lt;/h4&gt;

&lt;p&gt;Bartlett's 1932 &lt;em&gt;Remembering&lt;/em&gt; argued that recall is not reproductive but reconstructive, and that the reconstruction is shaped by the rememberer's existing schemata. Modern work, including the constructive episodic simulation hypothesis of Schacter and Addis, extends the picture: the same constructive machinery that builds memories also builds imagined futures, and in both directions the construction is shaped by the self-narrative the person already holds. The fact that this can go wrong, that people can recall with high confidence things that never happened, is not an objection to the underlying claim. The claim is that the self is the schema reconstruction works under, and a writer with no self writes without a schema at all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; Those three findings are not independent of each other. The self-reference effect is what you get when encoding is done under the self-schema. The Self Memory System is what that schema looks like when made architectural. Reconstructive recall is what retrieval does once the schema is in place. They are the same observation at three levels of detail, and the engineering consequence at every level is the same: a self is a relevance function for encoding, and a relevance function is what is missing from the write-time decision in every shipped agent memory system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That gives the self a job that is purely functional and has nothing to do with philosophy: &lt;strong&gt;it is the relevance function for encoding.&lt;/strong&gt; This is the version of the claim the agent case needs. It does not depend on a theory of consciousness or on a metaphysics of persons. It depends only on the observation that humans with a self pick better notes than entities without one, and on the engineering question of what in the agent corresponds to the self's job.&lt;/p&gt;

&lt;h3&gt;
  
  
  05. The Two Problems Are One Problem
&lt;/h3&gt;

&lt;p&gt;Now look at the agent's write-time problem from that angle. An agent asked to record what it learned has no basis for choosing. It cannot know the future query, which is a problem in its own right that takes more space than this argument can spare. But it also has no standing interests, no accumulated positions, no sense of what kind of thing it has needed before, because there is no it that has needed anything before. Every session's write decision is made by an entity with no history of caring about anything.&lt;/p&gt;

&lt;p&gt;That is why the notes come out generic or exhaustive. Those are the only two strategies available to a writer with no self: hedge toward the universal, or record everything and let the future sort it out. Both are rational. Neither is good. And no amount of prompting fixes it, because the missing input is not an instruction, it is a history.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Two symptoms, one cause.&lt;/strong&gt; Picture a junior hire on day one, before they have ever read the codebase, asked to write the team wiki for everything they learned in their first week. They will produce either anodyne entries that say nothing in particular or a verbatim transcript of every meeting. Both are the rational response of a person with no history of caring about the project, and both are useless. The fix is not a better prompt. The fix is a person who has been there six months and knows what matters, because by then they have positions and the positions decide the note.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;So the two problems are one problem.&lt;/strong&gt; Agents have no identity because they have no continuous memory. And they cannot write good memory because they have no identity to make encoding decisions on behalf of. Each absence sustains the other. Either absence is enough on its own; together they form a closed loop with no obvious place to start breaking it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 05. Entering the Loop
&lt;/h2&gt;

&lt;h3&gt;
  
  
  06. What the Entry Point Has to Be
&lt;/h3&gt;

&lt;p&gt;A cycle like that has to be entered somewhere, and the entry point is not a self-model. Writing "you are an agent who cares about X" into a prompt produces a description of a self, not a self, and the description has no causal connection to what actually gets encoded. You can put a personality in front of a writer with no history and the writer will still write like a writer with no history, because the personality is not load-bearing. It is a costume on the same engine.&lt;/p&gt;

&lt;p&gt;The entry point is the accumulated record of what this line of instances actually did: the decisions it made, the things it ruled out, the corrections it received, the mistakes it made twice. Not a personality, a track record. That is the material a self is made from, and unlike a prompt it is generated by the system's own history rather than asserted over it.&lt;/p&gt;

&lt;p&gt;Which suggests a concrete ordering rather than a philosophical program.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Store decisions and their reasons, not only findings.&lt;/strong&gt; A stance is what a later instance needs in order to inherit anything more than facts. The note kind that stores an architectural decision with its reasoning is the smallest material a self is made from. A store of bare findings is a library. A store that includes decisions with their reasons is closer to a track record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store corrections durably.&lt;/strong&gt; Being corrected is the single most identity-forming event available to a system, and the most perishable thing in the pipeline. A correction that survives only in the transcript of the session in which it was given is a correction the next instance never sees. The corrected mistake has to be as recoverable as the original decision it overrules, which means storing the correction as a first-class object with a date and an author.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the accumulated record inform what gets written next.&lt;/strong&gt; The write path has to see the record. A store whose writes are informed only by the current session cannot close the loop, because the next instance starts from the same blank state the last one did. The harness has to inject the relevant slice of the record at write time, not at recall time, and the slice has to be the slice that bears on what is being decided, not everything the store has ever held.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; None of those three steps requires solving personal identity. They require noticing that the record has to include the things a self would be made of, and today it mostly includes findings. The philosophical problem is real and the engineering problem is real, and they are not the same problem, but the engineering one is what you can actually do something about. The engineering case is what follows, and the philosophical part is there to give the engineering part its weight.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The store I work with day to day, vectr, has a note kind called &lt;code&gt;decision&lt;/code&gt; for this purpose, sitting alongside &lt;code&gt;directive&lt;/code&gt;, &lt;code&gt;gotcha&lt;/code&gt;, &lt;code&gt;finding&lt;/code&gt;, and &lt;code&gt;task&lt;/code&gt;. The decision kind is treated as a recall-only kind: it waits to be asked for, and when asked it returns its neighbours in chronological order, so an architectural decision is read as a sequence with the decisions that came before and after it. That is a small step toward the kind of record a later instance can inherit a stance from, and it is the kind of design choice that the field needs more of, and the kind the field mostly does not make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 06. Why It Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  07. Three Consequences That Are Not Philosophy
&lt;/h3&gt;

&lt;p&gt;Three consequences that are practical rather than conceptual, and that follow from the closed loop whether or not the philosophical case is interesting on its own.&lt;/p&gt;

&lt;h4&gt;
  
  
  Accountability needs a subject
&lt;/h4&gt;

&lt;p&gt;Holding a system to what it committed to yesterday presupposes something that persists to be held. With interchangeable instances there is no bearer: the thing that made the commitment is gone, and the thing in front of you never made it. Every framework for agent oversight, from regulatory regimes to internal review boards to a user trusting that yesterday's promise will still be honoured, quietly assumes a continuous subject, and none of the systems provide one. The accountability gap is not a future risk. It is the present state of the field, and the gap is exactly the size of the missing self.&lt;/p&gt;

&lt;h4&gt;
  
  
  Judgment is calibration against your own past errors
&lt;/h4&gt;

&lt;p&gt;A system that cannot remember being wrong cannot become better calibrated; it can only be made better by a better model. Those are different mechanisms, and only one of them is available to a running system. &lt;strong&gt;Capability scaling and learning from your own mistakes are different axes, and current systems have only the first.&lt;/strong&gt; The model can be upgraded. The history cannot, unless the system has somewhere to put it, and the something that puts it there has to be the same something that consults it later. That something is what the argument is calling a self.&lt;/p&gt;

&lt;h4&gt;
  
  
  Trust is a relationship with a particular entity
&lt;/h4&gt;

&lt;p&gt;A user builds trust with something that has a track record. If every session is a new instance with no continuity, the user is not building trust with an agent, only with a model. That may be fine, and it is the situation today. But it is a different thing, and it caps what the relationship can become. A user who trusts a model is trusting that the next instance will reproduce the relevant parts of the prior instances' behaviour. A user who trusts an agent is trusting a particular entity's history, and the latter is a stronger claim with different failure modes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; These three consequences are not independent. Accountability requires a bearer. Calibration against your own past requires that the bearer remember the past. Trust requires that the relationship is with a particular bearer across time. Take any one of the three seriously and the missing self becomes a missing load-bearing element of the design, not a footnote. The closed loop between no-self and bad-notes stops looking like a curiosity and starts looking like a structural reason the field's results are what they are.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 07. The Uncomfortable Objections
&lt;/h2&gt;

&lt;h3&gt;
  
  
  08. Continuity as Cost, Not Pure Win
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Is continuity desirable?&lt;/strong&gt; An agent that accumulates a self also accumulates its distortions: entrenched positions, self-serving reconstruction, the tendency to remember what fits the story. Human memory's identity-shaping is not free, and importing the mechanism plausibly imports the pathology. There is empirical reason to take the worry seriously. Memory conformity effects in collaborative recall, the misinformation effect, and the well-documented confabulation that follows damage to the hippocampal system all point the same way: a self that integrates memory does so at the cost of accuracy, and the cost is non-trivial. This deserves to be stated rather than waved away, and it is the strongest argument against the whole proposal.&lt;/p&gt;

&lt;p&gt;The reply that survives is honest about the trade. A writer with no self writes badly in a way that the field can already measure: notes that are generic, notes that are exhaustive, notes that get re-derived every session. A writer with a self writes with the distortions the literature documents, but at least the distortions are knowable and can be checked against the record. The current state has neither quality nor the chance of catching the absence of it. The proposal is not that continuity is free. It is that the absence of continuity is more expensive than the cost of having it, and that the cost is the kind of cost you can design around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this just personalization?&lt;/strong&gt; No, and the distinction matters. Personalization is a model adapting to a user. This is a system accumulating a history of its own actions and positions. The user is not the subject; the agent is. But the two are easy to conflate, and the distinction has to be kept clean. Personalization does not solve the write-time problem because the personalization is built on the user's history, not the agent's, and the agent that writes the note is still the agent with no history of its own. Personalization gives you a model that knows the user. What the loop needs is a model that knows itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does anything change if instances share the store?&lt;/strong&gt; Then you get one entity with many hands rather than many entities, which is a coherent design and possibly a better one. The claim here is not that private-per-line is correct, only that something has to make the later thing the same thing as the earlier one, and a shared pool with no continuity of stance does not. A team that shares a wiki but does not record why decisions were made has a wiki, not a team memory. A team that records decisions with reasons, even if the members rotate, has a team memory. The variable is not the number of hands. The variable is whether the store contains the kind of content a self would be made of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 08. What It Predicts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  09. The Three Tests, in Order
&lt;/h3&gt;

&lt;p&gt;The argument above is structural. The mechanism it proposes is a way out of the closed loop, and the three predictions below are how the mechanism earns its keep. None of them have been run. All three are specifiable, which is the useful part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 1.&lt;/strong&gt; Notes carrying a decision and its reasoning should be retrieved and acted on more often than notes carrying an equivalent bare finding, because the later instance can inherit a stance rather than re-deriving one. Measurable on an existing store with a decision kind already in it. The confound to control for is the fact that a writer who has chosen to store a decision has already done selection work, so any comparison should be between decision notes and finding notes of the same provenance and on the same topic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 2.&lt;/strong&gt; An agent given its own prior corrections should make the corrected mistake less often than one given the same information as a neutral fact. This isolates the identity-forming claim from the mere-information claim, and it is the load-bearing test. If provenance-as-my-own-past-error makes no behavioral difference, the ordering in Part 5 is wrong. Run this one first. It is the cheapest and it tests the actual mechanism rather than a downstream effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 3.&lt;/strong&gt; Note quality, measured as later retrieval usefulness, should improve as a store accumulates, if and only if the write path can see the accumulated record. A store whose writes are informed only by the current session should show flat note quality regardless of size. The curve is the test: a store that knows itself grows, a store that does not plateaus, and the difference is the visible signal of the loop starting to turn.&lt;/p&gt;

&lt;h4&gt;
  
  
  Interactive demo: Pick the notes, twice
&lt;/h4&gt;

&lt;p&gt;You are the agent's write path, deciding which notes to keep from a session. The two columns show the same session transcript and the same five candidate notes. The left column has no history panel: you are a writer with no self. The right column has the same session but you can see the line's prior decisions and corrections before you pick. Play both sides and notice whether the two picks come from the same part of the menu, or whether the history panel moved you. That is the load-bearing difference the loop is about, felt from the inside.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close. The Line to Keep
&lt;/h2&gt;

&lt;p&gt;An agent has no way to be the same agent twice, and that is not only a philosophical deficit. Without a "self" there is nothing to decide what was worth remembering, so the absence that prevents identity is the same absence that makes the notes bad. The two problems are one problem, and the loop has to be entered somewhere. The place it has to be entered is the record: store decisions with their reasons, store corrections durably, let the record inform the next write, and the loop starts turning in the useful direction. The track record is the material a self is made from, and unlike a prompt it is generated by the system's own history rather than asserted over it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An agent that knows what it has done is on the way to knowing who it is. An agent that knows who it is on the way to writing notes worth reading.&lt;/strong&gt; Neither of those is solved by a longer system prompt. Both are started by a record that is built to be inherited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Locke, J. &lt;em&gt;&lt;a href="https://www.gutenberg.org/files/10615/10615-h/10615-h.htm" rel="noopener noreferrer"&gt;An Essay Concerning Human Understanding&lt;/a&gt;&lt;/em&gt;, Book II, Chapter XXVII, "Of Identity and Diversity." Source of the memory theory of personal identity.&lt;/li&gt;
&lt;li&gt;Butler, J. "Dissertation I, Of Personal Identity," appended to &lt;em&gt;The Analogy of Religion&lt;/em&gt; (1736). Source of the circularity objection.&lt;/li&gt;
&lt;li&gt;Reid, T. &lt;em&gt;Essays on the Intellectual Powers of Man&lt;/em&gt; (1785). Source of the transitivity counterexample.&lt;/li&gt;
&lt;li&gt;Parfit, D. &lt;em&gt;Reasons and Persons&lt;/em&gt; (Oxford University Press, 1984). The modern Relation R view.&lt;/li&gt;
&lt;li&gt;Tulving, E. "Remembering and knowing the past." &lt;em&gt;American Scientist&lt;/em&gt; 77, 361-367, 1989. The episodic-versus-semantic distinction with the KC case study.&lt;/li&gt;
&lt;li&gt;Conway, M. A., and Pleydell-Pearce, C. W. "&lt;a href="https://doi.org/10.1037/0033-295X.107.2.261" rel="noopener noreferrer"&gt;The construction of autobiographical memories in the self-memory system&lt;/a&gt;." &lt;em&gt;Psychological Review&lt;/em&gt; 107(2), 261-288 (2000).&lt;/li&gt;
&lt;li&gt;Rogers, T. B., Kuiper, N. A., and Kirker, W. S. "&lt;a href="https://doi.org/10.1037/0022-3514.35.9.677" rel="noopener noreferrer"&gt;Self-reference and the encoding of personal information&lt;/a&gt;." &lt;em&gt;Journal of Personality and Social Psychology&lt;/em&gt; 35(9), 677-688 (1977).&lt;/li&gt;
&lt;li&gt;Symons, C. S., and Johnson, B. T. "&lt;a href="https://doi.org/10.1037/0033-2909.121.3.371" rel="noopener noreferrer"&gt;The self-reference effect in memory: A meta-analysis&lt;/a&gt;." &lt;em&gt;Psychological Bulletin&lt;/em&gt; 121(3), 371-394 (1997).&lt;/li&gt;
&lt;li&gt;Bartlett, F. C. &lt;em&gt;Remembering: A Study in Experimental and Social Psychology&lt;/em&gt; (Cambridge University Press, 1932).&lt;/li&gt;
&lt;li&gt;Schacter, D. L., and Addis, D. R. "&lt;a href="https://doi.org/10.1098/rstb.2007.2087" rel="noopener noreferrer"&gt;The cognitive neuroscience of constructive memory&lt;/a&gt;: remembering the past and imagining the future." &lt;em&gt;Philosophical Transactions of the Royal Society B&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Hu, Y., Liu, S., Yue, Y., et al. "&lt;a href="https://arxiv.org/abs/2512.13564" rel="noopener noreferrer"&gt;Memory in the Age of AI Agents&lt;/a&gt;." arXiv:2512.13564.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/agent-memory-trust-ladder/"&gt;Agent Memory Needs a Trust Ladder&lt;/a&gt;. The trust axis.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/a-guess-becomes-a-fact/"&gt;A Guess Becomes a Fact on the Next Read&lt;/a&gt;. The kind axis.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/agent-memory-harness-property/"&gt;The Agent Never Chooses to Remember&lt;/a&gt;. The harness-injection mechanism Part 5's third step depends on.&lt;/li&gt;
&lt;li&gt;vectr source: &lt;code&gt;[agent/working_context_store/_events.py](https://github.com/swapnanil/vectr/blob/main/agent/working_context_store/_events.py)&lt;/code&gt; and &lt;code&gt;[_store.py](https://github.com/swapnanil/vectr/blob/main/agent/working_context_store/_store.py)&lt;/code&gt; (note kinds &lt;code&gt;decision&lt;/code&gt;, &lt;code&gt;directive&lt;/code&gt;, &lt;code&gt;gotcha&lt;/code&gt;, &lt;code&gt;finding&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agentmemory</category>
      <category>personalidentity</category>
      <category>psychologicalcontinuity</category>
      <category>vectr</category>
    </item>
    <item>
      <title>Consolidation Is the Missing Phase in Agent Memory</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:36:18 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/consolidation-is-the-missing-phase-in-agent-memory-2488</link>
      <guid>https://dev.to/swapnanilsaha/consolidation-is-the-missing-phase-in-agent-memory-2488</guid>
      <description>&lt;p&gt;The field has one answer to running out of context, and that answer is more context. Windows went from thousands of tokens to hundreds of thousands to millions, and the next number is already being quoted. The implicit theory is that a large enough window makes the memory problem go away.&lt;/p&gt;

&lt;p&gt;Biological intelligence had the same problem and did not solve it that way. Working memory in humans is famously tiny, a handful of items, and it did not grow. What grew instead was a second process, running when nothing else is running, that decides which transient traces become durable and reorganises them as it does. The standard model in the systems-consolidation literature, formalised over decades by Squire and collaborators and extended through Diekelmann and Born's active-systems-consolidation work and Stickgold's sleep-replay studies, has the hippocampus hold recent memory and the cortex hold the long-term store, with the transfer driven by repeated reactivation during sleep and quiet wakefulness. The mechanism is supported, the role of sleep is supported, and the specific molecular choreography is still being argued about in the literature, including which sleep stages do which work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The missing primitive in agent memory is not capacity, it is a phase.&lt;/strong&gt; Agent systems have a write path and a read path and nothing in between. Nothing ever revisits what was stored, merges two notes that turned out to be the same fact, promotes a repeated observation into a general one, or drops what has been superseded. Storage happens at full speed during the task, judged by an agent that is busy doing something else, and then it is never touched again. That is not a small gap in the design. It is a whole stage of the pipeline that does not exist.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 1: The Diagnosis
&lt;/h1&gt;

&lt;h2&gt;
  
  
  01 · What a Window Buys, and What It Does Not
&lt;/h2&gt;

&lt;p&gt;A larger context window is genuinely useful and the argument here is not against one. It buys the ability to hold more of a problem at once, and problems that fit are easier than problems that do not. Everything that follows assumes the window is not a small one and still argues the gap remains.&lt;/p&gt;

&lt;p&gt;What a larger window does not buy is any of the following.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It does not decide what mattered.&lt;/strong&gt; Everything in a window is equally present, which sounds like an advantage until you notice that "equally present" is the same as "unranked". A window is a pile, not a structure. Nothing in the design says that the fact you wrote five minutes ago matters more than the one you wrote an hour ago, or that the fact you wrote once matters less than the one you wrote three times. The window does not know, and the model reading the window has no signal beyond recency to act on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It does not survive the session.&lt;/strong&gt; This is the obvious one and it is usually the only one people name. But note the shape of the failure: it is not that the information was too big, it is that the boundary discarded it wholesale, with no step that asked what was worth carrying. The compressed version of a session, what gets written into a continuation summary, is a single act of summarization with no further chance to recover what the summarizer decided did not matter. &lt;a href="https://swapnanilsaha.com/blog/what-survives-compact-claude-code/" rel="noopener noreferrer"&gt;"What Actually Survives /compact in Claude Code"&lt;/a&gt; measured this: across 108 and 138 forced compactions, the very first continuation summary carried zero of ten seeded facts in both arms, and the dropped facts stayed dropped through every subsequent summary. Capacity was not the issue. The summarizer had to fit the conversation into a smaller window and it made a one-shot judgment about what to keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It does not improve with use.&lt;/strong&gt; A window is the same window on the thousandth task as on the first. Nothing about it gets better at holding this particular codebase, this particular user, this particular recurring mistake. A memory system could, in principle, learn to weight the things that turn out to matter. A window cannot, because it carries no information across sessions and no signal during them about what is and is not worth keeping.&lt;/p&gt;

&lt;p&gt;Each of those is a different problem, and none of them gets smaller as the window grows. Scaling capacity addresses the first-order symptom and leaves the structure untouched.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; Capacity is the visible failure, which is why the field treats it as the problem. The actual problems are the ones a bigger window cannot reach: a pile of unranked items, a boundary that drops things wholesale, and a workspace that learns nothing from being used. None of them is a token-budget problem. All of them are pipeline-shape problems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is worth dwelling for a moment on the biological contrast, because the biology is where the design vocabulary already exists. Cowan's 2001 reconsideration in &lt;em&gt;Behavioral and Brain Sciences&lt;/em&gt; puts the working-memory capacity at roughly four chunks, with a typical range of three to five, revising Miller's classic seven plus or minus two by stripping out the benefit of chunking. The number is the most-cited modern estimate, and the exact figure depends on task conditions. What the biology did not do, when faced with the same pressure, was grow working memory. It built a second process for moving things out of working memory into a long-term store, and a third process for organising that long-term store while no retrieval or encoding was happening. The long-term store in humans is not a pile either; it is structured, and the structure is built offline.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 2: The Missing Phase
&lt;/h1&gt;

&lt;h2&gt;
  
  
  02 · What Consolidation Actually Does
&lt;/h2&gt;

&lt;p&gt;The word gets used loosely, so it is worth being precise about the operations, because the operations are what agent systems lack.&lt;/p&gt;

&lt;p&gt;Consolidation, in the standard model, is the set of transformations applied to already-stored traces during offline periods. The label covers synaptic consolidation, which stabilises individual traces at the cellular level on a fast timescale, and systems consolidation, which reorganises memory across the hippocampus and cortex on a slow one, with the two understood as complementary rather than competing. The standard reference, Squire, Stark and Clark's 2004 review of the medial temporal lobe and the later &lt;em&gt;Cold Spring Harbor&lt;/em&gt; consolidation paper, describes the broad picture: recent memories depend on the hippocampus, remote memories are supported by cortical sites, and the transfer is driven by repeated reactivation during slow-wave sleep and quiet wakefulness. Sleep's role is well supported; which sleep stages do which work, and how the molecular details run, are still being worked out in the literature, and that nuance is preserved here rather than papered over.&lt;/p&gt;

&lt;p&gt;Strip the biology and the operations are ordinary engineering. None of them is exotic. The reason they matter is the combination.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selection.&lt;/strong&gt; Not everything survives. Something decides what does. The biological version is partly spontaneous decay, partly activity-dependent stabilisation. In an engineering system, the decision is what the design says it is: keep what is reinforced, let the rest go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration.&lt;/strong&gt; A new trace is reconciled against what is already known, rather than appended beside it. The hippocampus-to-cortex picture is the canonical example: the new episode gets bound into the existing cortical schema instead of sitting as an isolated record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abstraction.&lt;/strong&gt; Repeated specifics become a general pattern, and the specifics can then be dropped or kept at lower priority. Schema formation is the standard model term for it. The generalisation is the thing that survives; the instances are the input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reorganisation.&lt;/strong&gt; Structure changes as a result, so later retrieval is against a different shape than the one that was written. The store is not a journal in chronological order; it is a graph whose connectivity reflects what has been learned.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now check each against a current agent memory system. &lt;strong&gt;Selection&lt;/strong&gt; happens once, at write time, by an agent guessing. &lt;strong&gt;Integration&lt;/strong&gt; does not happen. Two notes about the same fact sit as two notes forever, and a note that contradicts an older one is appended rather than reconciled. &lt;strong&gt;Abstraction&lt;/strong&gt; does not happen. Twenty observations of the same pattern remain twenty observations, and the pattern that would tie them together never gets written. &lt;strong&gt;Reorganisation&lt;/strong&gt; does not happen. The store's shape is the accumulated order of writes, which is write order, which is not the shape anyone would design for retrieval.&lt;/p&gt;

&lt;p&gt;The write path is doing all four jobs at once, badly, in the middle of a task, and then nothing revisits the result.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Analogy.&lt;/strong&gt; The biological picture is a librarian who arrives at night. During the day the library is open: people come in, read, take notes, and leave. Nobody stops to alphabetise the new arrivals, decide which notes deserve a second copy, or merge two index cards that turn out to describe the same book. The librarian does that work after the library closes, and the next morning the shelves are in a different shape. The agents are the readers. There is no librarian.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  03 · Why the Write Path Cannot Do This Job
&lt;/h2&gt;

&lt;p&gt;The instinct that says "the agent should just write better notes, integrating and abstracting as it goes" is the same instinct that says "make the window bigger." Both fail for structural reasons, not for lack of effort.&lt;/p&gt;

&lt;p&gt;At write time the agent has exactly one episode. Integration requires comparing against the rest of the store, which it has not read. Abstraction requires having seen the pattern repeat, which by definition has not happened yet on the first instance. Selection requires knowing what will be asked later, which is the thing nobody can know.&lt;/p&gt;

&lt;p&gt;All three operations need something the write path structurally does not have: &lt;strong&gt;a view over many episodes, at a time when no episode is in progress.&lt;/strong&gt; That is what makes consolidation a separate phase rather than a better version of an existing one. A single agent in a single task has the wrong vantage point by construction, and no amount of prompt engineering will give it the right one, because the right vantage point is the cross-section of every episode the store has ever seen.&lt;/p&gt;

&lt;p&gt;There is a second reason, and it is about incentives rather than information. During a task, the agent's objective is the task. Every token spent curating memory is a token not spent on the thing it was asked to do, and the benefit lands in some future session that the current one is not evaluated on. A process that only pays off later will always lose to the process being measured now. Consolidation has to run when there is no competing objective, which is precisely why the biological version runs offline. The match between "no competition for the work" and "no evaluation of the work" is the entire reason a phase is the right shape, not a job description.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning.&lt;/strong&gt; This is also why a smarter model will not fix the gap on its own. A better reasoner is still a better reasoner about the thing in front of it. It can write a slightly better note in slightly better prose, and the structural problem is unchanged: it is one episode, it is in the middle of a task, and it is being measured on the task. A model that thinks harder about its own memory during a task is a model that does the task worse, with a slightly tidier store nobody is using.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Part 3: The Distinction
&lt;/h1&gt;

&lt;h2&gt;
  
  
  04 · Compaction Is Not Consolidation
&lt;/h2&gt;

&lt;p&gt;A careful reader will say context compaction already is consolidation, and it is worth addressing head-on rather than ignoring, because the rest of the argument depends on the distinction holding.&lt;/p&gt;

&lt;p&gt;The two are related. The shared shape is: take a body of stored material, apply a transformation, keep the result. The shared failure mode is that the transformation is lossy. After that, they diverge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compaction&lt;/strong&gt; reduces a single in-flight transcript to fit a window. The summarization step at a compaction boundary sees one conversation, the one that just happened, and the output has to fit the smaller window the next turn will use. The unit of work is one transcript, the trigger is window pressure, and the output is a summary the next turn reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consolidation&lt;/strong&gt; operates across many stored episodes with no window pressure. The unit of work is the long-term store, the trigger is some offline condition (time, count, idle), and the output is a restructured store the next retrieval reads. The operation is allowed to merge two notes, drop a superseded one, and rebuild the index by which retrieval happens. The summariser at a compaction boundary is not allowed any of those things, because it has to produce a piece of text, not a reshaped store.&lt;/p&gt;

&lt;p&gt;The two differ in scope, in what triggers them, and in what they are allowed to do. A compaction reduces one transcript; a consolidation reshapes many. A compaction is a property of the harness; a consolidation is a property of the store. &lt;a href="https://swapnanilsaha.com/blog/what-survives-compact-claude-code/" rel="noopener noreferrer"&gt;"What Actually Survives /compact in Claude Code"&lt;/a&gt; measures the first. The system argued missing here is the second, and they are different objects.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Compaction&lt;/th&gt;
&lt;th&gt;Consolidation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;One in-flight transcript&lt;/td&gt;
&lt;td&gt;Many stored episodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;Window pressure&lt;/td&gt;
&lt;td&gt;Offline condition (time, count, idle)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;A summary the next turn reads&lt;/td&gt;
&lt;td&gt;A restructured store retrieval reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allowed to merge two notes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allowed to drop a note&lt;/td&gt;
&lt;td&gt;By not summarising it&lt;/td&gt;
&lt;td&gt;Yes, with evidence kept&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allowed to reorganise the store&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure mode&lt;/td&gt;
&lt;td&gt;Silent loss of un-summarised detail&lt;/td&gt;
&lt;td&gt;Silent loss of wrongly dropped item&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the distinction does not hold up, the central claim is wrong. The strongest reading of the distinction is that they live at different layers of the system: compaction is a property of the prompt-construction pipeline, consolidation is a property of the memory store. A system can do both, do one, or do neither. The systems that ship today do compaction and skip consolidation. The systems that do both are rare and narrow; the two that come closest are Letta and Zep, and they are worth a closer look, because the claim "agent systems have no consolidation" is a generalisation they complicate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Letta's sleep-time agent&lt;/strong&gt; is a real counter-example. Their sleep-time compute runs a second agent in the background that rewrites the primary agent's in-context memory blocks, summarises recent conversations into stable notes, and revises learned context. It is the closest published design to a consolidation phase this framing allows, and it is a narrow one: the operations are organised around the in-context memory blocks, the primary agent's surface of interaction, not around the long-term store as a whole. The generalisation, dedup, and structure-changing operations argued missing here are partly there for the in-context tier and partly not there for the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zep's Graphiti&lt;/strong&gt; runs community-level consolidation: it clusters entities, summarises clusters, and refreshes the cluster summaries when new information shifts them. The community-detection work happens during ingestion through a dynamic extension of label propagation, with periodic full refreshes to correct drift. This is closer to the biology's structure-formation step, in the same "reorganise the store" sense, but it operates on a graph of entities and facts rather than on the notes an agent writes about its own work. The two systems do different slices of what is being described. Neither performs the full combination: dedup across the long-term store, abstraction of repeated observations, and structure-changing reorganisation, all in one offline process.&lt;/p&gt;

&lt;p&gt;So the claim narrows. &lt;strong&gt;Most production agent memory systems have no offline phase that merges, abstracts, and reorganises the long-term store.&lt;/strong&gt; Letta and Zep are real and partial counter-examples, and they are worth studying precisely because they show how much of the design problem is left even after the offline phase exists. What is being argued is about the rest, and the rest is most of the field.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 4: The Design Problem
&lt;/h1&gt;

&lt;h2&gt;
  
  
  05 · The Judgments a Phase Has to Make
&lt;/h2&gt;

&lt;p&gt;Saying "add a consolidation phase" is easy. The hard part is that it has to make judgments, and each one is a place where it could be wrong in a way nobody notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What merges.&lt;/strong&gt; Two notes that say the same thing should become one. But "the same thing" is exactly the judgment that a near-duplicate check gets wrong in both directions. Too loose and it destroys a distinction that mattered; too tight and it does nothing. Merging is lossy and, unlike a retrieval miss, it is not recoverable. A merged note is harder to undo than a forgotten one, because part of the evidence that supported keeping them separate was in the two notes' differences, and those differences are gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What generalises.&lt;/strong&gt; Twenty instances of a pattern should become one statement of the pattern, and the twenty should probably go. But the generalisation is an inference, and it inherits every problem of an unlabeled inference. If it is stored with the same authority as the observations it replaced, a wrong generalisation is now a fact with no visible provenance and no surviving counter-evidence. The honest design says: keep the generalisation, keep the instances, and rank the generalisation below the instances, so any specific case can override it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is dropped.&lt;/strong&gt; This is the one nobody wants to build, and the next section is about why it is unavoidable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What gets restructured.&lt;/strong&gt; If consolidation reorganises the store, then retrieval is running against a shape that was not the shape at write time. That is the point, but it means a note's future retrievability is being changed by a process the writer never saw. A note that was highly retrievable because it was the most-recent observation of something can become unretrievable the moment twenty newer observations push it out of the active window. That is the right outcome if the newer observations are better, and the wrong outcome if they are not. There is no version of this that is not a judgment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; None of these is a reason not to build the phase. They are the reason it is a design problem rather than a cron job. A batch script can dedup; a cron can summarise; neither of them has a view of which decisions the store will regret, because the store has not yet had the chance to be wrong in a way that becomes visible. The phase needs to be instrumented, auditable, and reversible, in roughly that order of priority.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  06 · The Uncomfortable Part: Forgetting
&lt;/h2&gt;

&lt;p&gt;Every operation above reduces. Merging reduces two to one. Abstraction reduces twenty to one and discards the twenty. Selection keeps some and not others. Forgetting is the substance of consolidation, not a side effect.&lt;/p&gt;

&lt;p&gt;A consolidation phase that only adds is not consolidation. It is a second write path, and a second write path makes the pile grow faster. So building this means building deliberate, principled loss into a system whose entire current design treats loss as failure. Retrieval misses are at least visible as empty results. Something consolidated away is not visible as anything: the store returns a confident, well-organised answer that is missing the case it dropped last week, and nothing in the response says so. The harness sees a working memory layer; the user sees an agent that knows what it knows. The dropped item is invisible because it is gone, and being gone is exactly what makes the design defensible and exactly what makes the failure mode silent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning.&lt;/strong&gt; Silent loss is the failure mode. The honest version of the proposal is not "add consolidation and memory gets better." It is: &lt;strong&gt;memory cannot get better without a phase that reduces, and reduction is the most dangerous operation in the system.&lt;/strong&gt; Any serious design has to say what it keeps evidence of, what becomes irreversible, and how a wrongly-dropped item could ever be noticed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three design rules follow from taking that seriously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep evidence, not the original.&lt;/strong&gt; A merged note keeps both sources in an audit row, with the merge's reason and actor. A generalised note keeps the instances, demoted, with the generalisation labelled as an inference. A dropped note keeps a tombstone carrying the reason, the actor, and the timestamp, so the system can answer "did we ever know X" even when "X" is not retrievable as a current fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reversible by default.&lt;/strong&gt; If consolidation is append-only, like the note lifecycle in the &lt;a href="https://swapnanilsaha.com/blog/reinstatement-is-the-missing-primitive/" rel="noopener noreferrer"&gt;revocation design&lt;/a&gt; on this site, a wrong consolidation decision is one more event, not a destroyed row. That is the property that lets a consolidation policy be aggressive, because the cost of being wrong is one undo, not an irretrievable loss. An irreversible policy has to be right; a reversible one only has to be right often enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the drop visible at retrieval.&lt;/strong&gt; A wrongly-dropped item is invisible at the moment it would be useful. The only reliable way to surface it is to ask the system, after a session in which something should have come up and did not, what it has dropped recently. That is a query the agent rarely thinks to make, and a query the harness could be wired to make. The system that noticed the absence is the &lt;a href="https://swapnanilsaha.com/blog/absence-is-the-memory-nobody-writes/" rel="noopener noreferrer"&gt;same one&lt;/a&gt; that has to be made to notice the consolidation failure.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 5: The Larger Claim
&lt;/h1&gt;

&lt;h2&gt;
  
  
  07 · On the Path to General Capability
&lt;/h2&gt;

&lt;p&gt;The argument so far is an engineering one. Here is the larger claim, stated so it can be disagreed with.&lt;/p&gt;

&lt;p&gt;A system that cannot reorganise what it knows cannot get better at knowing. It can get better at reasoning, because that is the model, and models are improving quickly. But its knowledge stays an undigested pile of episodes in write order, and every new task pays the full cost of finding what is relevant in that pile. The reasoning gets sharper on the same evidence; the evidence does not accumulate.&lt;/p&gt;

&lt;p&gt;Competence and accumulation are different axes. Scaling the model moves the first. Nothing currently moves the second, because the only mechanism that could, a process that revisits and restructures what was stored, is absent from most production designs. The two real counter-examples, Letta's sleep-time agent and Zep's community consolidation, are partial: they cover parts of the long-term store and miss other parts, and the miss is structural, not a bug to be fixed by a patch.&lt;/p&gt;

&lt;p&gt;That is why this is not merely an optimisation. A system whose knowledge never consolidates is one that starts from the same structural position on task one thousand as on task one. It may perform better, because the model is better, but it has not accumulated anything. Whatever general capability turns out to require, it is hard to believe it is compatible with never digesting your own experience.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; The shape of the argument is not "the model is not good enough." It is "the model is good enough that the missing infrastructure is the part that limits long-term capability." The two are easy to confuse. The first invites a model improvement. The second invites an infrastructure one. They have different costs, different timelines, and different chances of working.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One honest limit needs stating. The argument is structural, not measured. There is no controlled experiment here showing that a consolidator improves an agent's capability on held-out tasks. What is identified is a missing piece, the reasons it is missing, and the literature the analogy points at. The measurement would be: take an existing store, run a merge-and-abstract pass over it, and measure retrieval precision and downstream task accuracy before and after on a held-out set. If the pass helps, the missing phase was load-bearing. If it does not, the answer is somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  08 · What This Predicts
&lt;/h2&gt;

&lt;p&gt;Three empirical claims follow from the argument, and each one is testable on a store that already exists. The experiment is not run here, but it is specifiable, and specifiability is the part that turns a structural claim into a falsifiable one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive demo (in the HTML version).&lt;/strong&gt; A store before and after one consolidation pass. Twelve notes from one agent across eight sessions, in write order. Two pairs are near-duplicates the agent did not notice. One observation repeats four times. One generalisation the system could have made is missing. Toggle the consolidation pass to see what merge, abstraction, and reorganisation each do, and which notes survive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The three predictions, stated so they can be disagreed with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 1.&lt;/strong&gt; In a memory store with no consolidation, the proportion of notes that are near-duplicates of an existing note should grow with store size, and grow superlinearly with the number of sessions. Nothing removes them and every session can add one. This is measurable on any existing store today: write a near-duplicate detector, sample the store, and plot duplicate share against note count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 2.&lt;/strong&gt; Retrieval precision should degrade as the store grows, holding the query set fixed, because unconsolidated near-duplicates compete for the same slots. If precision is flat with store size, the pile is not actually costing anything and this argument is much weaker. &lt;a href="https://swapnanilsaha.com/blog/relevance-is-the-wrong-sort-order/" rel="noopener noreferrer"&gt;"Relevance Is the Wrong Sort Order for Agent Memory"&lt;/a&gt; points at the same shape from a different angle: ranking a pile by similarity cannot but make the pile look bigger than it is, and the cost is paid at retrieval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 3.&lt;/strong&gt; An offline merge-and-abstract pass over an existing store should improve retrieval on a held-out query set without a model change. This is the load-bearing one: it tests whether the missing phase is worth anything, and it can be run on a store that already exists. It needs no new architecture, only a batch job and an honest before-and-after.&lt;/p&gt;

&lt;p&gt;Prediction 3 is where to start. The other two are diagnostics. If prediction 1 is not borne out, the whole argument loses its quantitative premise. If prediction 2 is not borne out, the pile turns out to be free and the argument is about a non-problem. If prediction 3 is borne out, the missing phase is the lever. None of the three requires a model improvement, which is the point: the missing piece is not in the model, it is in the pipeline around the model, and the pipeline is the part an engineer can build today.&lt;/p&gt;

&lt;h1&gt;
  
  
  Close: The Line to Keep
&lt;/h1&gt;

&lt;p&gt;The window got a thousand times bigger and the memory problem did not go away, because capacity was never the missing piece. What is missing is the phase where a system decides, when nothing else is happening, what its own experience was worth. Selection, integration, abstraction, reorganisation. Selection so the pile does not grow forever. Integration so two notes that mean the same thing stop competing for the slot. Abstraction so twenty observations of a pattern stop being twenty facts. Reorganisation so retrieval is running against the shape of what the system knows, not the shape of what the system once wrote.&lt;/p&gt;

&lt;p&gt;It is harder to build than to describe, because every one of those operations is lossy and lossy is the failure mode the rest of the system is built to avoid. The way through is the same one the rest of the agent-memory design has settled on: keep evidence of what changed, make the change reversible, and let the system notice when a wrong change has slipped through. The biology has been doing this for a long time, badly enough that the role of sleep is still being argued about, well enough that the shape of the operation has not changed in decades. The agent-memory field is at the point where it could borrow the shape, and the borrow is overdue.&lt;/p&gt;

&lt;h1&gt;
  
  
  Sources and Further Reading
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Squire, L. R., Stark, C. E. L., Clark, R. E. &lt;a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2686941/" rel="noopener noreferrer"&gt;"The Medial Temporal Lobe"&lt;/a&gt;. &lt;em&gt;Annual Review of Neuroscience&lt;/em&gt;, 2004. The standard reference for the medial temporal lobe and the systems-consolidation picture; introduces the hippocampus-as-temporary-store, cortex-as-long-term-store model that the analogy in Section 01 rests on.&lt;/li&gt;
&lt;li&gt;Squire, L. R., Genzel, L., Wixted, J. T., Morris, R. G. &lt;a href="https://cshperspectives.cshlp.org/content/7/8/a021667" rel="noopener noreferrer"&gt;"Memory Consolidation"&lt;/a&gt;. &lt;em&gt;Cold Spring Harbor Perspectives in Biology&lt;/em&gt;, 2015. The review that consolidates the synaptic-and-systems distinction and the current state of the field; the claim in Section 02 that mechanisms are still being worked out comes from here.&lt;/li&gt;
&lt;li&gt;Diekelmann, S., Born, J. &lt;a href="https://pubmed.ncbi.nlm.nih.gov/20837518/" rel="noopener noreferrer"&gt;"The memory function of sleep"&lt;/a&gt;. &lt;em&gt;Nature Reviews Neuroscience&lt;/em&gt;, 2010. Active systems consolidation framework: reactivation during SWS, schema integration, the slow-timescale redistribution borrowed in Section 02 for the analogy.&lt;/li&gt;
&lt;li&gt;Cowan, N. &lt;a href="https://pubmed.ncbi.nlm.nih.gov/11515286/" rel="noopener noreferrer"&gt;"The magical number 4 in short-term memory: a reconsideration of mental storage capacity"&lt;/a&gt;. &lt;em&gt;Behavioral and Brain Sciences&lt;/em&gt; 24(1), 2001, pp. 87-114. The modern working-memory capacity estimate: roughly 4 chunks (3-5) when chunking is prevented, revising Miller's 7 plus or minus 2. The number used in Section 01 for the biological contrast.&lt;/li&gt;
&lt;li&gt;Rasmussen, P., Paliychuk, P., Beauvais, T., Ryan, J., Chalef, D. &lt;a href="https://arxiv.org/abs/2501.13956" rel="noopener noreferrer"&gt;"Zep: A Temporal Knowledge Graph Architecture for Agent Memory"&lt;/a&gt;. arXiv:2501.13956, January 2025. The closest published agent-memory system to consolidation: community-level clustering of entities with dynamic label propagation during ingestion and periodic full refreshes. Cited in Section 04.&lt;/li&gt;
&lt;li&gt;Letta. &lt;a href="https://www.letta.com/blog/sleep-time-compute" rel="noopener noreferrer"&gt;"Sleep-Time Compute"&lt;/a&gt;. Letta blog. The sleep-time agent design: a second agent in the background that rewrites the primary agent's in-context memory blocks, summarises recent conversations, and revises learned context. The other real counter-example, also in Section 04.&lt;/li&gt;
&lt;li&gt;Berry, J. "When Agent Memory Needs Sleep." &lt;a href="https://usewire.io/blog/when-agent-memory-needs-sleep/" rel="noopener noreferrer"&gt;Wire Blog&lt;/a&gt;. A field-side argument that consolidation is the same shape as compaction applied to the memory store rather than the conversation; the distinction in Section 04 takes the shared shape seriously and then argues the differences dominate.&lt;/li&gt;
&lt;li&gt;Saha, S. &lt;a href="https://swapnanilsaha.com/blog/what-survives-compact-claude-code/" rel="noopener noreferrer"&gt;"What Actually Survives /compact in Claude Code"&lt;/a&gt;. Neighbour essay on this site. The measured 100+ compaction map; cited in Section 01 for the measured shape of the boundary and in Section 04 for the explicit distinction between compaction and consolidation.&lt;/li&gt;
&lt;li&gt;Saha, S. &lt;a href="https://swapnanilsaha.com/blog/reinstatement-is-the-missing-primitive/" rel="noopener noreferrer"&gt;"Reinstatement Is the Missing Primitive in Agent Memory"&lt;/a&gt;. Neighbour essay on this site. The append-only note lifecycle and the reversibility property borrowed in Section 06 for the design rules of a reversible consolidator.&lt;/li&gt;
&lt;li&gt;Saha, S. &lt;a href="https://swapnanilsaha.com/blog/absence-is-the-memory-nobody-writes/" rel="noopener noreferrer"&gt;"Agents Never Remember What Isn't There"&lt;/a&gt;. Neighbour essay on this site. The mechanism for noticing a wrongly dropped item that Section 06 needs: the absence is the only signal a consolidation failure leaves behind, and it is exactly what an absence-tracked system can be made to surface.&lt;/li&gt;
&lt;li&gt;Saha, S. &lt;a href="https://swapnanilsaha.com/blog/relevance-is-the-wrong-sort-order/" rel="noopener noreferrer"&gt;"Relevance Is the Wrong Sort Order for Agent Memory"&lt;/a&gt;. Neighbour essay on this site. The case that ranking an unconsolidated pile by similarity makes the pile look bigger than it is, which is the quantitative premise Prediction 2 rests on.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agentmemory</category>
      <category>memoryconsolidation</category>
      <category>cognitivescience</category>
      <category>llmagents</category>
    </item>
    <item>
      <title>The Writer Cannot Know the Reader's Question</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Wed, 09 Sep 2026 18:33:39 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/the-writer-cannot-know-the-readers-question-9ih</link>
      <guid>https://dev.to/swapnanilsaha/the-writer-cannot-know-the-readers-question-9ih</guid>
      <description>&lt;p&gt;Ask why agents write bad notes and the answers are all about effort. The model was lazy. The prompt was weak. It did not think about what would be useful later. Write better notes and the problem goes away.&lt;/p&gt;

&lt;p&gt;This misdescribes the task. Writing a note is not summarizing what happened. It is placing a bet on a question that has not been asked yet, by a reader who does not exist yet, in a context nobody can see. The writer is asked to optimize for a target that is not merely unknown but &lt;strong&gt;unknowable at the time the decision is made&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Write-time precision in agent memory is not a hard problem solved badly. It is an &lt;strong&gt;ill-posed&lt;/strong&gt; problem being answered anyway, and the outputs look like bad estimates because there is nothing else they could look like. The fix is not a better writer. The fix is a different unit of storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The Asymmetry
&lt;/h2&gt;

&lt;h3&gt;
  
  
  01. Why Write Is Not Retrieve
&lt;/h3&gt;

&lt;p&gt;The asymmetry is structural rather than practical.&lt;/p&gt;

&lt;p&gt;Retrieval is evaluable. Given a query and a corpus, there is a fact of the matter about which items are relevant. You can label it, measure it, and improve against the measurement. This is why information retrieval has a century of methodology and agent memory keeps borrowing from it.&lt;/p&gt;

&lt;p&gt;Writing is not evaluable in the same way, because the value of a note is not a property of the note. &lt;strong&gt;It is a property of the pairing between the note and a future query.&lt;/strong&gt; At write time, one half of that pair does not exist. Not "is hard to predict." Does not exist. There is no distribution to estimate because the events that will generate the queries have not happened.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight:&lt;/strong&gt; Write-time precision in agent memory is undefined, not hard. The writer is not making a bad estimate. It is being asked to optimize a target that has no value to estimate, because the future queries have not been sampled from a distribution that has not been fixed. A better guess does not exist, however carefully the writer tries.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The information-science literature has had a name for this for sixty years: &lt;strong&gt;anticipatory indexing&lt;/strong&gt;. Robert Fairthorne, writing in the 1960s, made the same observation about a human indexer preparing entries for an open-ended set of future queries. The consequence the indexing literature does not draw is the one that matters here: a design consequence the indexing literature does not draw: if the writer cannot know the question, the unit of storage should change.&lt;/p&gt;

&lt;p&gt;Compare the writer's task to a genuinely hard prediction problem, like forecasting demand for a product. Demand forecasting is hard but it is well posed: there is a real distribution, it is stationary enough to sample, and yesterday's data informs tomorrow's estimate. A wrong forecast is checkable, and a useful one improves against the check. Note writing has none of these. The next session's queries depend on what the user decides to work on next, which depends on things outside the system entirely, and there is no recurring unit against which the writer's performance can be measured.&lt;/p&gt;

&lt;p&gt;This places the task close to what Frank Knight called &lt;strong&gt;uncertainty&lt;/strong&gt; in the 1921 sense, but not identical to it. Knightian uncertainty names a situation where the decision maker has no probability distribution; the writer's predicament has that, but it adds a second defect that the term does not capture. The writer has no feedback signal either. Not "I cannot estimate" but "no estimate I produce will be checked." That second defect is what makes the task ill-posed, not merely uncertain. A Knightian decision can still be evaluated by its consequences; a write-time guess about an unknown future query cannot, because the relevant consequence is the future query that does not yet exist.&lt;/p&gt;

&lt;p&gt;So the writer is not doing a hard estimation task badly. It is doing an ill-posed task, and the outputs look like bad estimates because nothing else is available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Why The Standard Advice Fails
&lt;/h2&gt;

&lt;h3&gt;
  
  
  02. The Two-Pole Failure
&lt;/h3&gt;

&lt;p&gt;Watch what happens when the standard instructions to a memory writer are unpacked, because each one smuggles in the missing information rather than supplying it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Write down what will be useful later&lt;/em&gt; requires knowing what later looks like. &lt;em&gt;Write down the important parts&lt;/em&gt; requires an importance function, which is defined relative to a purpose that has not been set. &lt;em&gt;Write it so you could pick up where you left off&lt;/em&gt; assumes the next session resumes this work, which is frequently false: it may be a different task in the same repository, or the same task after the code changed underneath it. None of these is wrong exactly. Each is &lt;strong&gt;underdetermined&lt;/strong&gt;, and the writer resolves the underdetermination by guessing, because guessing is the only thing available.&lt;/p&gt;

&lt;p&gt;The observable consequence is a specific and recognizable failure pattern. Agent notes tend toward one of two poles.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight:&lt;/strong&gt; Both poles are rational responses to an unknown target. A generic note hedges by being applicable to many possible futures. An over-specific note hedges by recording everything in reach and letting the future sort it out. Neither is laziness. They are the two available strategies when you must write for a reader you cannot see, and they fail in opposite directions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The generic pole writes things that are true but not discriminating. &lt;em&gt;The auth module handles authentication.&lt;/em&gt; &lt;em&gt;There is a test suite.&lt;/em&gt; Future relevance search will not be able to tell those notes apart from each other or from the noise of the codebase itself, and they will land in retrieval results at roughly the rate of the words they contain. They are cheap to write and cheap to ignore.&lt;/p&gt;

&lt;p&gt;The over-specific pole writes the exact state of a moment that will never recur. &lt;em&gt;Line 340 currently returns None here.&lt;/em&gt; &lt;em&gt;Last build failed at 14:02 with a TypeError in user_service.py.&lt;/em&gt; These are accurate as photographs, but the value of a photograph decays with the distance from the moment it captured, and the writer cannot know how far away the moment is from the moment that will want it.&lt;/p&gt;

&lt;p&gt;That both poles are visible in published work is the part worth checking. The LLM evaluation literature now routinely reports a failure pattern it names &lt;em&gt;over-general and over-specific&lt;/em&gt; output as a single joint phenomenon: the same model, on the same prompt, can produce a generation that is true but too generic to act on, or specific to a moment that does not exist. Whether published evaluation work already treats the two failure modes as one calibration problem is a question worth settling; the pattern here is an observation from practice rather than a cited result. What no treatment does is locate the source of the bimodality in the write-time problem rather than in the model.&lt;/p&gt;

&lt;p&gt;That claim is an observation from practice, not a result, and it is a prediction: if the framing here is right, an existing store's notes, classified as generic, over-specific, or well-scoped, will show a bimodal distribution with a thin middle. Section 06 turns the prediction into a test.&lt;/p&gt;

&lt;h3&gt;
  
  
  03. The Missing Signal
&lt;/h3&gt;

&lt;p&gt;There is a practical consequence that is worse than the philosophical one, and it is what keeps the failure pattern self-perpetuating.&lt;/p&gt;

&lt;p&gt;Because a note's value is realized at retrieval, the feedback signal for writing arrives late, arrives rarely, and arrives attached to the wrong event. A note written on Monday and usefully retrieved on Friday produces a success credited to Friday's retrieval, not to Monday's write. A note written on Monday and never retrieved produces nothing: no signal, no error, no record that a bet was placed and lost. There is no equivalent of the empty retrieval basket: the basket is invisible because there is no basket.&lt;/p&gt;

&lt;p&gt;This is what makes the write path nearly untunable, and it explains a structural pattern visible in nearly every long-lived agent memory store. Retrieval can be tuned because retrieval failures are visible and attributable. A wrong note that surfaces in response to the wrong query is named by the query, ranked against the right one, and the gap between them is measurable. Writing cannot be tuned the same way because writing failures are silent and their cost lands somewhere else, later, attributed to something else. A session that should have remembered a fact and re-derived it instead looks like a slow session, not a memory failure. A session that should not have remembered a fact and acted on it anyway looks like a model hallucination, not a memory over-supply.&lt;/p&gt;

&lt;p&gt;The observable consequence is the accretion of write-side heuristics that never get removed. A rule about what to store, once added, is nearly impossible to evaluate, because evaluating it would require knowing what the counterfactual store would have retrieved. The rules accrete because nothing can prove them wrong. A system that began with three rules of what to write will have thirty within a year, and the more rules there are, the less each one can be checked against actual retrieval outcomes, because retrieval outcomes never reveal which rule was responsible for the note that was or was not written.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; The write path has nearly no learning signal. Retrieval fails noisily, with a query, a ranking, and a measurable gap. Writing fails silently, with a note that is never matched to a future query that would have wanted it. Any system whose quality is measured only on the retrieval side is being measured on its better-instrumented half, and improvements to the worse-instrumented half will look like improvements even when they are not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The diagnosis has a design consequence, and it is not &lt;em&gt;give up&lt;/em&gt;. It is that the unit of memory is wrong. If the writer cannot know the question, it should not be storing an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: The Design Move
&lt;/h2&gt;

&lt;h3&gt;
  
  
  04. Store a Condition, Not an Answer
&lt;/h3&gt;

&lt;p&gt;The alternative is to store the condition under which the note becomes relevant, rather than a guess about which future query will match it.&lt;/p&gt;

&lt;p&gt;Not &lt;em&gt;this is important&lt;/em&gt; but &lt;em&gt;this matters when someone touches this file&lt;/em&gt;. Not &lt;em&gt;remember the auth flow&lt;/em&gt; but &lt;em&gt;surface this when the auth module is being edited&lt;/em&gt;. The note's content is preserved; the unit of storage now includes a triggering condition attached to the content rather than a guess about future relevance attached to the writer's confidence.&lt;/p&gt;

&lt;p&gt;This is a real change in what is being predicted, and it is worth being careful about why it is easier, rather than assuming the change is cosmetic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What changes, stated as a prediction.&lt;/strong&gt; Let the writer's task be to predict a function. The two candidates differ in the shape of their domain:&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;relevance:   f:  Queries   &amp;amp;rarr;  {match, no match}
condition:   g:  Circumstances  &amp;amp;rarr;  {fire, no fire}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first domain is open-ended: every session can ask anything. The second is enumerable: the set of files, operations, and lifecycle stages in a workspace is known and small. Predicting &lt;em&gt;which words the reader will use, at what level of abstraction, in pursuit of which purpose&lt;/em&gt; is the original ill-posed task. Predicting &lt;em&gt;whether this artifact will be touched in some future work&lt;/em&gt; is a question with a real distribution, drawn from the workspace's actual activity. The writer is still predicting. It is predicting over a space it can actually observe the shape of.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The argument is &lt;strong&gt;not&lt;/strong&gt; that the condition is guaranteed to fire. The argument is that the writer is now betting on a smaller, more stable, more checkable space, and the bet is one the writer can be wrong about in observable ways.&lt;/p&gt;

&lt;p&gt;There is a second and less obvious benefit. A triggering condition is falsifiable at write time in a way a relevance guess is not. &lt;em&gt;This matters when someone edits the auth module&lt;/em&gt; can be checked immediately for coherence: does that file exist, is it the right file, is the condition ever going to fire. &lt;em&gt;This will be useful later&lt;/em&gt; cannot be checked at all. Moving from an unfalsifiable claim to a falsifiable one is the actual improvement here, independent of whether retrieval works better downstream.&lt;/p&gt;

&lt;p&gt;This connects directly to the falsifiability of stored directives. &lt;a href="https://swapnanilsaha.com/blog/a-directive-is-not-a-memory/" rel="noopener noreferrer"&gt;A Directive Is Not a Memory&lt;/a&gt; argued that a stored rule's only falsifiable core is the present condition that was thrown away at write time, and that memory systems which store prose instead of predicates cannot test the rules they carry. The same shape applies one level up: a stored note's only falsifiable part is the triggering condition, and memory systems which store importance guesses instead of conditions cannot evaluate the notes they carry. The mechanism is the same. The part worth keeping is the part that can be checked today, and the part typically thrown away is the part that could have been.&lt;/p&gt;

&lt;p&gt;The proposal is also not new in isolation. Event-condition-action rules in active database systems, publish-subscribe routing in message brokers, and prospective memory in cognitive psychology are all prior art for storing &lt;em&gt;do this when that happens&lt;/em&gt; as a first-class object rather than as an afterthought. The novelty being claimed here is the application to agent memory, the argument that the unit &lt;strong&gt;should&lt;/strong&gt; be the condition rather than the answer, and the observation that this changes the falsifiability status of what is stored. The active-database tradition formalized the trigger side decades ago; the agent memory tradition has not yet adopted it as a primary unit. The literature on production rule systems solved the related problem of conditioned actions, and the conditioning construct is decades old. What is new is the application, not the underlying idea.&lt;/p&gt;

&lt;p&gt;To see why the unit change is not just a relabeling, run the same idea through a lens argued elsewhere. &lt;a href="https://swapnanilsaha.com/blog/search-is-memorys-write-path/" rel="noopener noreferrer"&gt;Search Is Memory's Write Path&lt;/a&gt; argued that agent search and agent memory are two phases of one system, the first search being the write and every repeat the read, and that the write path is shaped by what the retrieval mechanism will later surface. That is not contradicted here. The narrower claim is that even with perfect awareness of the retrieval mechanism, the writer still cannot know the specific future query, and so the unit of storage should be a triggering condition whose firing is observable, rather than a guess about future relevance whose correctness is not. Search being the write path is consistent with conditions being the unit; the conditions become what the write path produces, and the retrieval mechanism selects among them on the circumstances that have actually fired.&lt;/p&gt;

&lt;h3&gt;
  
  
  05. What It Does Not Solve
&lt;/h3&gt;

&lt;p&gt;The honest limits, because the argument is weaker than it may sound and the right place to be weak is up front.&lt;/p&gt;

&lt;p&gt;Condition-based memory has its own failure mode, and it is not a milder one. A condition that never fires stores a note that is never seen, and unlike a relevance miss, nobody is even querying for it. The note sits in the store, the trigger it carries never resolves true, and the system cannot detect the gap because the gap is the absence of an event, not the presence of a wrong one. A wrong condition is a permanent silent loss rather than a ranking error, and it is at least defensible to argue that the trade is a measurable failure for an unmeasurable one. The bet is that the unmeasurable failure has lower expected cost because the space of conditions is smaller and more stable than the space of queries, but the bet is not a theorem.&lt;/p&gt;

&lt;p&gt;The proposal also does not cover the genuine case where a reader asks an open question with no triggering circumstance at all. Someone wanting to know &lt;em&gt;what was learned about this subsystem last month&lt;/em&gt; is doing exactly the query-shaped retrieval that cannot be predicted for. Conditions do not help there. Relevance search remains the right tool, and the right design treats conditions and relevance as two subsystems that carry different loads. The proposal is that conditions should carry the load they can carry, not that they replace retrieval. Replacing one with the other is the same failure in a different shape.&lt;/p&gt;

&lt;p&gt;And the move &lt;strong&gt;moves&lt;/strong&gt; rather than &lt;strong&gt;removes&lt;/strong&gt; the burden of foresight. Choosing a condition is still a prediction. The claim is only that it is a prediction over a smaller, more stable, more checkable space, and that this is a genuine improvement rather than a relabeling. That claim could be wrong. Section 06 says how you would find out.&lt;/p&gt;

&lt;h3&gt;
  
  
  06. The Three Predictions
&lt;/h3&gt;

&lt;p&gt;The argument is structural rather than measured. Three predictions would distinguish it from the alternatives, and they are stated so a future test can confirm or refute each one independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 1.&lt;/strong&gt; Notes written with a triggering condition should show a higher retrieval rate than notes written without one in the same store, controlling for content. If condition-carrying notes are retrieved no more often, the central proposal fails. This is a one-line count, run on any working store that already records retrieval events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 2.&lt;/strong&gt; Agreement between independent writers should be higher on conditions than on importance. Give several agents the same session and ask each to say what is important, then ask each to say when it will matter. If the second task produces more agreement, that is evidence the prediction space really is more constrained. If agreement is equally poor, Section 04's core argument is unsupported.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 3.&lt;/strong&gt; The two-pole pattern in Section 02 should be measurable. Classify an existing store's notes as generic, over-specific, or well-scoped. The argument predicts the middle category is small and the distribution is bimodal. A healthy unimodal distribution of well-scoped notes would falsify the framing directly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Which prediction is the cheap one.&lt;/strong&gt; Prediction 2 is the discriminating test and the cheapest to run. It tests the actual mechanism rather than the downstream effect, and it does not require a working implementation to evaluate. The setup is roughly: pick a session whose facts are unambiguous, give the same transcript to N models or N fresh sessions of the same model, and ask the two questions. Inter-rater agreement on &lt;em&gt;when will this matter&lt;/em&gt; is the number that decides whether the framing survives.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No measurement is reported here. The point of the predictions is that the argument can be settled, not that it has been.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close: The Line to Keep
&lt;/h2&gt;

&lt;p&gt;The writer is asked to be precise about a question that has not been asked. That is not a hard problem being solved badly. It is an ill-posed problem being answered anyway, and the fix is not a better guess about the question but a note that does not need to guess it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The unit of memory should be a triggering condition, not an answer.&lt;/strong&gt; The condition is the part that can be checked today, the part that survives the writer's missing information about the future, and the part that turns the write path from silent into falsifiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Information retrieval: anticipatory indexing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fairthorne, R. A. &lt;em&gt;Content Analysis, Indexing, and Matching&lt;/em&gt;. Foundational essay in Fairthorne's work on the aboutness problem, treating indexing as the construction of a document representation against an open set of future queries. Source of the anticipatory-indexing framing used in Section 01. Publicly discussed in the Journal of Documentation tradition, 1960s.&lt;/li&gt;
&lt;li&gt;Brittain, J. M. &lt;em&gt;Information and Its Retrieval&lt;/em&gt;. Modern treatment of anticipatory indexing and the indexer-for-unknown-uses problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Belief revision: the recovery postulate&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alchourrón, C. E., Gärdenfors, P., Makinson, D. &lt;em&gt;On the Logic of Theory Change: Partial Meet Contraction and Revision Functions&lt;/em&gt;. Journal of Symbolic Logic 50(2), 1985, pp. 510-530. The contraction operator and the partial-meet construction, and the formal definition of contraction used in Section 05.&lt;/li&gt;
&lt;li&gt;Gärdenfors, P. &lt;em&gt;Knowledge in Flux: Modeling the Dynamics of Epistemic States&lt;/em&gt;. MIT Press, 1988. Introduces the recovery postulate for contraction as an additional constraint. Section 05 weakens it, on the grounds that the agent memory case lacks the feedback signal it assumes.&lt;/li&gt;
&lt;li&gt;Stanford Encyclopedia of Philosophy, &lt;em&gt;Belief Revision&lt;/em&gt; (Hansson, major revisions through 2023). Standard reference frame for the AGM postulates, including the formulation of recovery and the subsequent critiques that proposed weaker constraints in its place. &lt;a href="https://plato.stanford.edu/entries/belief-revision/" rel="noopener noreferrer"&gt;https://plato.stanford.edu/entries/belief-revision/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Decision theory: uncertainty and ill-posedness&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Knight, F. H. &lt;em&gt;Risk, Uncertainty and Profit&lt;/em&gt;. Boston: Houghton Mifflin, 1921. The distinction between measurable risk and unmeasurable uncertainty. Section 01 uses the term in its 1921 sense, and adds a second-order defect: the missing feedback signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;LLM evaluation: over-general and over-specific output&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production rule systems: event-condition-action&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Widom, J., Finkelstein, A. &lt;em&gt;Set-Oriented Production Rules in Relational Database Systems&lt;/em&gt;. Proceedings of the 1990 ACM SIGMOD International Conference on Management of Data. The HiPAC lineage of active databases that produced the ECA formalism, the prior art for condition-first storage.&lt;/li&gt;
&lt;li&gt;Chakravarthy, S. et al. &lt;em&gt;An ECA Rule-based Framework for Active Information Systems&lt;/em&gt;. Active databases literature of the 1990s, where the conditioning construct was already solved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Companion posts in this series&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/search-is-memorys-write-path/" rel="noopener noreferrer"&gt;Search Is Memory's Write Path&lt;/a&gt;. The argument about retrieval shaping what gets stored. Section 04 cites the distinction: retrieval shapes the write path, and the open question is what unit that shaped write path should produce.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/a-directive-is-not-a-memory/" rel="noopener noreferrer"&gt;A Directive Is Not a Memory&lt;/a&gt;. The argument about the falsifiability of stored rules. Section 04 cites the parallel: the same shape one level up, applied to memory notes rather than directives.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/search-is-memorys-write-path/" rel="noopener noreferrer"&gt;Search Is Memory's Write Path&lt;/a&gt;: the companion argument. Retrieval shapes what gets stored. The argument here is that even with perfect awareness of the retrieval mechanism, the writer still cannot know the specific future question, so the unit of storage should change from an answer to a triggering condition.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/a-directive-is-not-a-memory/" rel="noopener noreferrer"&gt;A Directive Is Not a Memory&lt;/a&gt;: the parallel at the directive level. A stored rule's only falsifiable core is the present condition that was thrown away at write time. The same shape one level up, applied to memory notes rather than rules.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/relevance-is-the-wrong-sort-order/" rel="noopener noreferrer"&gt;Relevance Is the Wrong Sort Order for Agent Memory&lt;/a&gt;: the layer above. Once the unit is a condition, the ranker has to surface conditions on their firing circumstances, not on similarity to a query. A condition is not similar to a future event, it is true at a future event.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/agent-memory-harness-property/" rel="noopener noreferrer"&gt;The Agent Never Chooses to Remember&lt;/a&gt;: the argument for delivering the resolved conditions from the harness rather than asking the agent to fetch them. The cue-anchored trigger machinery in Section 04 is the implementation path for condition-first storage.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agentmemory</category>
      <category>informationretrieval</category>
      <category>llmagents</category>
      <category>developertools</category>
    </item>
    <item>
      <title>Recall Is a Write Operation: Why Usage-Weighted Memory Ranking Cannot Discover It Was Wrong</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Tue, 08 Sep 2026 14:05:39 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/recall-is-a-write-operation-why-usage-weighted-memory-ranking-cannot-discover-it-was-wrong-5a6l</link>
      <guid>https://dev.to/swapnanilsaha/recall-is-a-write-operation-why-usage-weighted-memory-ranking-cannot-discover-it-was-wrong-5a6l</guid>
      <description>&lt;h1&gt;
  
  
  Recall Is a Write Operation: Why Usage-Weighted Memory Ranking Cannot Discover It Was Wrong
&lt;/h1&gt;

&lt;p&gt;Ask an agent memory store what it knows about a topic and it hands back a ranked list. A query goes in, a small set of notes comes out, nothing changes. That is how retrieval is described, how it is drawn in architecture diagrams, and how almost everyone reasons about it.&lt;/p&gt;

&lt;p&gt;It is false in every memory system that has a notion of usage. Recall touches a last-accessed timestamp. It feeds a decay or reinforcement score. It updates a counter that a future ranking will read. The read is a write, and the thing it writes into is the input of the next read.&lt;/p&gt;

&lt;p&gt;That closes a loop. Once a loop is closed, the interesting question is no longer "does this rank well." It is "what does this converge to."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A memory system that reinforces what it retrieves cannot discover that it was wrong, because the mechanism that would demote a bad note is the same mechanism the bad note keeps winning.&lt;/strong&gt; This is not a tuning problem. It is a structural property of any ranking that takes its own past output as an input, and it has to be designed against rather than tuned away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The Write in the Read
&lt;/h2&gt;

&lt;h3&gt;
  
  
  01. Why Usage Writes in the First Place
&lt;/h3&gt;

&lt;p&gt;The usage write is not an accident or an implementation detail somebody forgot to remove. It is load-bearing, and it is there because the alternatives are worse.&lt;/p&gt;

&lt;p&gt;A memory store needs to decide what to keep. Storage is finite, retrieval gets noisier as the corpus grows, and a store that never forgets becomes a store that cannot find anything. So it needs a signal for what matters.&lt;/p&gt;

&lt;p&gt;The available signals are poor. Age is a bad proxy for validity, and a note is not less true for being old. Explicit importance labels are unreliable, because whoever writes the note is guessing about a future they cannot see. Content-based scoring cannot tell an important note from a verbose one.&lt;/p&gt;

&lt;p&gt;Usage is genuinely the best available signal. A note that keeps getting retrieved is, on the face of it, useful. Reinforcing it is the obvious move, and it is roughly what human memory does. Reinforced retrieval strengthens the retrieved item and, as a side effect, the related unpractised items lose retention; Anderson, Bjork, and Bjork named this in 1994 and called it retrieval-induced forgetting. The mechanism is the same: retrieval writes to memory. This is why practically every agent memory system does it, and it is why the argument here is not that the write should be removed.&lt;/p&gt;

&lt;p&gt;The argument is that the write closes a loop, that closed loops have dynamics, and that almost nobody has looked at what these dynamics are.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; Usage is the strongest available signal for what matters in a store, and writing to it is the right default. The write itself is not the target. The target is treating it as a passive side effect, because a write that feeds back into its own input is a different object from one that does not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 2: The Shape
&lt;/h2&gt;

&lt;h3&gt;
  
  
  02. The Loop, Stated Plainly
&lt;/h3&gt;

&lt;p&gt;Write it out as a cycle. A note is retrieved. Retrieval updates its usage. Usage feeds ranking. Ranking determines what is retrieved next.&lt;/p&gt;

&lt;p&gt;Every arrow is uncontroversial on its own. Together they form a positive feedback loop with no negative term. What is retrieved becomes more retrievable, which causes it to be retrieved, which makes it more retrievable.&lt;/p&gt;

&lt;p&gt;The same shape shows up in classical feedback systems, in preferential attachment networks, and in the popularity-bias literature on recommender systems. The general result, in all three, is concentration: a small number of items capture a disproportionate share of attention, and the concentration is shaped substantially by early, arbitrary events rather than by intrinsic quality.&lt;/p&gt;

&lt;p&gt;Applied to memory: whichever note happened to surface first for a class of query acquires an advantage that is self-sustaining, regardless of whether it was the best answer. Not because the ranking is bad, but because the ranking is recursive.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Analogy.&lt;/strong&gt; Think of a citation index. A paper that gets cited early is more visible, so it gets cited more, so it stays visible. New papers in the same field have to overcome an index that already points at the established one. The field's actual quality distribution gets distorted by a visibility process that has nothing to do with quality, and the distortion compounds. Agent memory with usage-weighted ranking is the same process, run inside a single store, with a single consumer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The competitor is not merely behind. It is behind in a way that compounds, and it has no mechanism to catch up, because catching up requires being retrieved and being retrieved requires having caught up.&lt;/p&gt;

&lt;h3&gt;
  
  
  03. The Concentration That Comes from Preferential Attachment
&lt;/h3&gt;

&lt;p&gt;The closest formal analogue is preferential attachment, the mechanism Barabasi and Albert proposed in 1999 to explain the scale-free degree distributions observed in real networks. The mechanism is plain: at each step a new node arrives and connects to an existing node with probability proportional to that node's current degree. The more connected a node is, the more likely it is to receive the next link.&lt;/p&gt;

&lt;p&gt;Three properties of the model matter here, and each is stated explicitly in the source.&lt;/p&gt;

&lt;p&gt;First, the model produces scale-free degree distributions of the form P(k) ~ k^-3, in the limit. The model is a positive feedback system in which initially random variations are automatically reinforced, and the Wikipedia summary of the model, drawing on the original paper, names this "the rich get richer."&lt;/p&gt;

&lt;p&gt;Second, the reinforcement is unbounded without intervention. Concentration that begins as small initial advantage ends as permanent structure, because every step multiplies the imbalance.&lt;/p&gt;

&lt;p&gt;Third, and the one that is easy to miss: &lt;strong&gt;both growth and preferential attachment are required&lt;/strong&gt;. The model with growth alone produces a geometric distribution. The model with preferential attachment alone produces something that decays toward a Gaussian. The scale-free property emerges from the combination. Memory stores with usage-weighted ranking have both: notes are added over time, and retrieval probability is proportional to past retrieval count. The shape that produces concentration in Barabasi-Albert networks is the same shape that produces concentration in memory stores.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning.&lt;/strong&gt; The argument is a structural analogy, not a proof. A memory store's retrieval counts are not a power law, and the exact statistics of concentration depend on details the model does not contain. The claim is the shape of the dynamic, not its quantitative predictions, and Part 6 takes the claim apart into falsifiable predictions so the analogy can be tested rather than waved at.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The connection runs through the same math because the same mechanism is at work: a quantity that grows in proportion to its current value, with no negative term, in a system that keeps adding new elements. Memory stores have all three. So do citation indices, social networks, and the long tail of any ranking system whose history feeds forward into the next ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: Where It Hurts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  04. When the Winning Note Is Wrong
&lt;/h3&gt;

&lt;p&gt;Concentration alone is not obviously bad. If the popular note is the right note, a system that surfaces it faster over time is a system that is improving. The failure is what happens when the popular note is wrong.&lt;/p&gt;

&lt;p&gt;For a wrong note to be corrected, something has to expose it. Typically that means retrieving a competing note, or retrieving the wrong note in a context where its wrongness becomes evident. Both require the wrong note's competitor to be retrievable.&lt;/p&gt;

&lt;p&gt;But the wrong note has been winning, so it has the usage advantage, so it keeps winning. The competitor has been losing, so it has decayed, so it keeps losing. The system's error-correction path runs through exactly the mechanism the error has captured.&lt;/p&gt;

&lt;p&gt;This is the sharp point, and it is worth stating without hedging. &lt;strong&gt;The wrongness of the note is not what determines whether it survives. Its retrieval history is.&lt;/strong&gt; Those two are correlated at the start and decouple immediately, because after the first retrieval the note's rank is being driven by its own past rank rather than by any property of the world.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; The mechanism is a winner-take-all filter applied to correctness. A note that is right enough to win once wins forever. A note that loses once loses forever, even if it was right all along and the winning note was wrong. The filter has no correction signal, because the only signal it reads is its own output.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A system with this shape does not fail loudly. It converges, and it looks like it is getting better while it does, because concentration is easy to mistake for confidence. The measured behavior of a memory store settling into a small set of reliably-surfaced notes is the same measured behavior whether that set is right or wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://swapnanilsaha.com/blog/reinstatement-is-the-missing-primitive/" rel="noopener noreferrer"&gt;Reinstatement Is the Missing Primitive in Agent Memory&lt;/a&gt; makes the same point from a different angle: the store accumulates beliefs that nobody was willing to touch, and the symptom is invisible from inside the system. What is added here is the mechanism that makes the accumulation inevitable rather than incidental.&lt;/p&gt;

&lt;h3&gt;
  
  
  05. Decay and Exploration Are Not the Fix
&lt;/h3&gt;

&lt;p&gt;The usual response is that decay handles this. Old notes lose weight, so a stale wrong note eventually falls away and the competitor gets a chance.&lt;/p&gt;

&lt;p&gt;Decay does not break the loop. It changes its time constant.&lt;/p&gt;

&lt;p&gt;Decay penalizes notes that are not retrieved. The wrong note is being retrieved, so it is not decaying. The competitor is not being retrieved, so it is. Decay is not a counterweight to the feedback loop; it is the same loop with the sign flipped, acting on the loser. It accelerates the concentration it was introduced to prevent.&lt;/p&gt;

&lt;p&gt;Decay only helps if the wrong note stops being retrieved, and the whole problem is that it does not.&lt;/p&gt;

&lt;p&gt;There is a second, subtler version of this mistake. Adding a small random exploration term so that low-ranked notes occasionally surface is a real mitigation and it is better than nothing. Epsilon-greedy and Upper Confidence Bound are the two named treatments in the bandit literature, both with optimal-order guarantees under stationary reward distributions. But note what they concede. If the fix for a ranking system is to sometimes ignore its own ranking, the ranking has stopped being a statement about relevance and become a statement about history. Exploration is a patch on a corrupted signal, not a repair of it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning.&lt;/strong&gt; Exploration cannot repair a signal that is downstream of itself. The bandit framing is sharp precisely because it concedes the problem: any policy that needs an explicit exploration term to perform well is a policy whose greedy arm is not the answer. A memory store whose primary signal is its own past output is in exactly that position, and no choice of epsilon closes the gap between "occasionally correct by luck" and "correct by construction."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both fixes (decay and exploration) treat the symptom. The disease is that ranking reads its own history, and the only cure is a ranking that does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 4: The Measurement Trap
&lt;/h2&gt;

&lt;h3&gt;
  
  
  06. The Error Is Self-Concealing
&lt;/h3&gt;

&lt;p&gt;Here is what makes this genuinely hard rather than merely subtle.&lt;/p&gt;

&lt;p&gt;Suppose you want to know whether your memory store has this pathology. The obvious check is whether retrieved notes are useful. But usefulness is judged by the agent, using the retrieved notes, in a context shaped by the retrieved notes. The evaluation is downstream of the thing being evaluated.&lt;/p&gt;

&lt;p&gt;Worse, the notes that would prove the store is wrong are precisely the ones the store has stopped returning. Their absence is invisible, because a retrieval that does not happen produces no output to inspect. You cannot see a miss.&lt;/p&gt;

&lt;p&gt;So the pathology is self-concealing in two independent ways at once. The evidence against it is suppressed by the mechanism, and the metrics for detecting it are computed from the mechanism's own output. A store deep in this failure mode and a store working perfectly look identical from the inside.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Insight.&lt;/strong&gt; This is the part that defeats the obvious fix. You cannot compute a metric from retrieval logs that detects the loop, because any such metric is inside the loop. The instrument and the thing being measured share their input, and the instrument will report success on the only data it can see. The only honest measurement is one whose input is causally outside the loop: a controlled comparison across stores with different usage histories, run on a held-out evaluation set that the store has never seen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The standard recommendation in the popularity-bias literature, where the same self-concealment problem has been studied for two decades, is to evaluate on a randomized exposure set: serve a uniform-random sample of items to a fraction of users and measure whether the items the model never recommends have similar quality to the items it does. The analog for agent memory is a held-out query set where retrieval is forced to be uniform across the candidate pool, and the evaluation asks whether the unpicked notes would have been better. The recommendation is unfashionable because it lowers the metric on every store that has been quietly winning, and a system that does not have the problem has nothing to fear from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 5: Breaking the Loop
&lt;/h2&gt;

&lt;h3&gt;
  
  
  07. Four Designs That Open It
&lt;/h3&gt;

&lt;p&gt;If the problem is that ranking reads its own history, the fix is to make some part of the ranking causally independent of retrieval history. Four shapes follow, offered as design directions rather than as finished solutions: none has been measured to validate the structural argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate the signals.&lt;/strong&gt; Usage can inform eviction, which is about what to keep, without informing ranking, which is about what to return. These are different questions and it is not obvious they should share a score. Coupling them is what closes the loop; decoupling them opens it, at the cost of needing a second signal for ranking. The published evaluation of MemX is consistent with this split: it tracks a retrieval count and a last-retrieved timestamp, both reported as inputs to a four-factor re-ranking that includes similarity, recency, frequency, and importance, with the frequency weight small (0.05) and the recency weight modest (0.25). The architecture does not feed the user's retrieval outcome back into the ranker in a self-referential way; the same paper reports that recency and time-decay are essentially uninformative as a primary signal, which is the natural outcome of treating usage as an eviction hint rather than as the rank.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make corrections structural rather than competitive.&lt;/strong&gt; If a note that corrects another is linked to it rather than merely ranked against it, the correction does not have to win a popularity contest to be seen. It arrives with the thing it corrects. &lt;a href="https://swapnanilsaha.com/blog/relevance-is-the-wrong-sort-order/" rel="noopener noreferrer"&gt;Relevance Is the Wrong Sort Order for Agent Memory&lt;/a&gt; argues for this on different grounds, and a linked successor independently dissolves the loop: it cannot be starved by it, because it is not competing in the ranking at all. &lt;a href="https://swapnanilsaha.com/blog/reinstatement-is-the-missing-primitive/" rel="noopener noreferrer"&gt;Reinstatement Is the Missing Primitive in Agent Memory&lt;/a&gt; makes the parallel point for the lifecycle: a note that is revoked and then proven correct can be reinstated precisely because reinstatement is an append, not a popularity contest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit from outside the loop.&lt;/strong&gt; Periodically evaluate notes against the world rather than against usage. Check whether a note's claim still holds. This is expensive and only works for checkable claims, but it is the only signal in this list that is genuinely exogenous. A store that audits a fraction of its notes against ground truth on a schedule can detect the loop from the outside, by measuring how its in-store accuracy compares to a non-usage-based benchmark. The result is a measurement, and the measurement is the only thing that can tell you whether the structural fixes above are actually working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instrument the concentration directly.&lt;/strong&gt; Even without a fix, measuring how concentrated retrieval is over time, and how much of that concentration is explained by early ordering rather than by content, would tell you whether the dynamic is live in a given store. This is the cheapest thing on the list and, as far as I can tell, nobody publishes it. The measurement is straightforward: take the per-query retrieval count for each note, sort it, and report the share of retrievals captured by the top 1 percent of notes. A store in which that share is increasing session-over-session is in the loop. A store in which the share is stable or tracking the query distribution is not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning.&lt;/strong&gt; None of the four above has been measured here. The argument is structural. A field that ships memory systems with usage-weighted ranking as the default should publish a measurement that confirms the structural argument or that overturns it, and the absence of such a measurement is itself part of the problem the argument names.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 6: Falsification
&lt;/h2&gt;

&lt;h3&gt;
  
  
  08. Three Predictions, in Priority Order
&lt;/h3&gt;

&lt;p&gt;An argument that cannot be tested is not an argument. The structural claim here produces three concrete predictions, and they are testable in a way the loop's self-concealment does not defeat: the predictions are about comparative behavior across stores, not about behavior on a single store's logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 1.&lt;/strong&gt; In a memory store with usage-weighted ranking, the distribution of retrievals across notes should become more concentrated over time, and more concentrated than the distribution of queries would justify. The second clause is the real test: concentration that tracks query concentration is just the store working. A simple measurement: for each store, compute the Gini coefficient of the retrieval distribution and the Gini coefficient of the query distribution, and report their difference across sessions. A growing difference is the loop running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 2.&lt;/strong&gt; Injecting the same note into two otherwise-identical stores at different initial positions should produce durably different long-run retrieval rates. If initial position washes out, the loop is weak and this argument is overstated. The experiment is cheap: seed two stores with the same note corpus, perturb the initial rank of one note in one of them, run a fixed workload of queries, and measure retrieval rates at the end. The two notes with the same content and different initial positions should have different long-run retrieval rates, and the size of the difference is the size of the loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 3.&lt;/strong&gt; A known-wrong note that has accumulated retrieval history should be harder to displace than a known-wrong note with none, holding content constant. This isolates history from quality, and it is the cleanest available falsification. If displacement difficulty does not depend on history, the mechanism described here is not operating.&lt;/p&gt;

&lt;p&gt;Prediction 2 is the one worth running first. It is cheap, it needs no ground truth about correctness, and it directly measures whether history dominates content. The interactive demo above is a small version of the same comparison: a one-query, ten-step simulation showing how the same note, placed at different initial ranks, accumulates different retrieval counts over a fixed workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close: The Line to Keep
&lt;/h2&gt;

&lt;p&gt;A memory system that reinforces what it retrieves is not learning what matters. It is learning what it retrieved. Those diverge from the first read, and the system has no way to notice, because the evidence that would tell it is the exact thing it has stopped returning.&lt;/p&gt;

&lt;p&gt;This is not the same argument as &lt;a href="https://swapnanilsaha.com/blog/relevance-is-the-wrong-sort-order/" rel="noopener noreferrer"&gt;Relevance Is the Wrong Sort Order for Agent Memory&lt;/a&gt;. That essay argued that ranking notes independently is wrong because notes carry causal relationships to each other, and a similarity sort drops the very note that carries the truth of a correction. The claim here is separate and compatible: ranking is not even a stable function, because it consumes its own output. The two are about different failure modes. The earlier one is about what the ranker chooses between. This one is about how what the ranker chooses feeds back into the next ranker's choice. Closing retrieval on lifecycle edges is the fix the earlier essay argues for. Keeping the edge that records what has been retrieved out of the ranker in the first place is the fix argued here.&lt;/p&gt;

&lt;p&gt;The store is being asked two questions at once. The first is what to remember, and usage is a defensible signal. The second is what to recall, and usage is not a defensible signal for the thing it is being asked to recall about. A store that conflates the two is, quietly, deciding tomorrow from yesterday and calling the decision relevance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Barabasi, A.-L., Albert, R. &lt;a href="https://www.science.org/doi/10.1126/science.286.5439.509" rel="noopener noreferrer"&gt;Emergence of Scaling in Random Networks&lt;/a&gt;. &lt;em&gt;Science&lt;/em&gt; 286(5439), 1999. The original preferential-attachment model; P(k) ~ k^-3 degree distribution; explicit statement that "initially random variations are automatically reinforced." &lt;a href="https://en.wikipedia.org/wiki/Barab%C3%A1si%E2%80%93Albert_model" rel="noopener noreferrer"&gt;Wikipedia summary&lt;/a&gt; reproduces the model's stated limitations: growth and preferential attachment are both required for the scale-free property. Part 2.&lt;/li&gt;
&lt;li&gt;Anderson, M. C., Bjork, R. A., Bjork, E. L. &lt;a href="https://en.wikipedia.org/wiki/Retrieval-induced_forgetting" rel="noopener noreferrer"&gt;Remembering can cause forgetting: Retrieval dynamics in long-term memory&lt;/a&gt;. &lt;em&gt;Journal of Experimental Psychology: Learning, Memory, and Cognition&lt;/em&gt; 20(5), 1994. The first demonstration of retrieval-induced forgetting; the original Rp+ / Rp- / NRp paradigm. &lt;a href="https://en.wikipedia.org/wiki/Retrieval-induced_forgetting" rel="noopener noreferrer"&gt;Wikipedia summary&lt;/a&gt; reproduces the three-category design. Part 1.&lt;/li&gt;
&lt;li&gt;Roediger, H. L., Karpicke, J. D. &lt;a href="https://en.wikipedia.org/wiki/Testing_effect" rel="noopener noreferrer"&gt;Test-Enhanced Learning: Taking Memory Tests Improves Long-Term Retention&lt;/a&gt;. &lt;em&gt;Psychological Science&lt;/em&gt; 17(3), 2006. The standard reference for the testing effect that motivates reinforcement of retrieved items. &lt;a href="https://en.wikipedia.org/wiki/Testing_effect" rel="noopener noreferrer"&gt;Wikipedia summary&lt;/a&gt; reproduces the spaced-repetition amplifications. Part 1, for the connection to human memory and the limits of the analogy.&lt;/li&gt;
&lt;li&gt;Sutton, R. S., Barto, A. G. &lt;em&gt;Reinforcement Learning: An Introduction&lt;/em&gt;. MIT Press, second edition 2018. The standard reference for epsilon-greedy exploration in multi-armed bandits; Chapter 2 covers the exploration-exploitation trade-off. &lt;a href="https://en.wikipedia.org/wiki/Multi-armed_bandit" rel="noopener noreferrer"&gt;Wikipedia summary&lt;/a&gt; of the bandit problem and the regret bounds. Part 3.&lt;/li&gt;
&lt;li&gt;Auer, P., Cesa-Bianchi, N., Fischer, P. &lt;a href="https://en.wikipedia.org/wiki/Multi-armed_bandit" rel="noopener noreferrer"&gt;Finite-time Analysis of the Multiarmed Bandit Problem&lt;/a&gt;. &lt;em&gt;Machine Learning&lt;/em&gt; 47, 2002. The original UCB1 analysis with the O(sqrt(KT ln T)) regret bound; the optimistic-in-the-face-of-uncertainty framing. Part 3.&lt;/li&gt;
&lt;li&gt;Du, P. &lt;a href="https://arxiv.org/abs/2603.07670" rel="noopener noreferrer"&gt;Memory for Autonomous LLM Agents: Mechanisms, Evaluation, and Emerging Frontiers&lt;/a&gt;. arXiv:2603.07670, 2026. The survey that names "self-reinforcing error" as the central risk of reflective memory; the API-always-errors example. Part 3.&lt;/li&gt;
&lt;li&gt;Chhikara, P., Khant, D., Aryan, S., Singh, T., Yadav, D. &lt;a href="https://arxiv.org/abs/2504.19413" rel="noopener noreferrer"&gt;Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory&lt;/a&gt;. arXiv:2504.19413, April 2025. Production memory architecture; ADD/UPDATE/DELETE/NOOP update phase. Cited in Part 5 as a production system whose architectural choices are consistent with the decoupling argument, without making the claim that the system has been measured against it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Recommender_system" rel="noopener noreferrer"&gt;Recommender system&lt;/a&gt;. Wikipedia article covering popularity bias, the self-contamination of training data, and the reproducibility gap between offline and online evaluation. Part 3 and Part 4.&lt;/li&gt;
&lt;li&gt;Wu, D., Wang, H., Yu, W., Zhang, Y., Chang, K.-W., Yu, D. &lt;a href="https://arxiv.org/abs/2410.10813" rel="noopener noreferrer"&gt;LongMemEval: Benchmarking Chat Assistants on Long-Term Interactive Memory&lt;/a&gt;. ICLR 2025, arXiv:2410.10813. Five abilities including knowledge updates; 500 questions; the reported 30 percent accuracy drop across long interactions. Cited in Part 4 as the empirical shape of the recency problem, not the mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/relevance-is-the-wrong-sort-order/" rel="noopener noreferrer"&gt;Relevance Is the Wrong Sort Order for Agent Memory&lt;/a&gt;. Argues that the unit of retrieval should be a trajectory, not a ranked set, and that the lifecycle edges on notes are not being used at retrieval time.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/reinstatement-is-the-missing-primitive/" rel="noopener noreferrer"&gt;Reinstatement Is the Missing Primitive in Agent Memory&lt;/a&gt;. The missing reverse edge of revocation. A memory store that cannot reverse a wrong correction accumulates bad beliefs, and the structural reason is the feedback loop analysed in Part 2.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agentmemory</category>
      <category>retrieval</category>
      <category>feedbackloops</category>
      <category>vectr</category>
    </item>
    <item>
      <title>A Guess Becomes a Fact on the Next Read</title>
      <dc:creator>Swapnanil Saha</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:33:55 +0000</pubDate>
      <link>https://dev.to/swapnanilsaha/a-guess-becomes-a-fact-on-the-next-read-5cna</link>
      <guid>https://dev.to/swapnanilsaha/a-guess-becomes-a-fact-on-the-next-read-5cna</guid>
      <description>&lt;p&gt;Everything an agent knows arrives as text in a context window. A user instruction is text. A tool result is text. A file the agent read is text. The agent's own speculation, written out mid-reasoning, is text. By the time any of it reaches the next turn, it is all the same type.&lt;/p&gt;

&lt;p&gt;That flattening is invisible while the conversation is live, because position and recency stand in for provenance. The user's instruction is the thing at the top. The guess is the thing the model just wrote. Nobody needs a label because the structure carries it.&lt;/p&gt;

&lt;p&gt;Memory removes the structure. A note is retrieved into a fresh context with no conversation around it, no position, no ordering, no author. What was a hedged inference at write time arrives at read time as a free-standing assertion, indistinguishable from something the user said outright.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dominant failure mode of agent memory is not forgetting. It is laundering.&lt;/strong&gt; A lost note costs a re-derivation, which is bounded and usually cheap. A note that silently changed epistemic status costs every decision that trusts it, and it leaves no trace of having changed. Systems built to maximize recall are optimizing the wrong quantity, because the expensive error is not the note that failed to come back. It is the note that came back wearing a confidence it never had.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 1: The Flattening
&lt;/h1&gt;

&lt;h2&gt;
  
  
  01 · Four Things That Are the Same Type and Should Not Be
&lt;/h2&gt;

&lt;p&gt;Consider four statements that could all end up in an agent's memory store, written during the same session.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user said the deploy script must never run on a Friday.&lt;/li&gt;
&lt;li&gt;The test suite emitted &lt;code&gt;FAILED tests/test_auth.py::test_expiry&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The agent read &lt;code&gt;config.yaml&lt;/code&gt; and it contained &lt;code&gt;timeout: 30&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The agent reasoned that the timeout is probably too low for the CI runner, since the failing test waits on a network call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first is a &lt;em&gt;normative&lt;/em&gt; instruction from a human. It cannot be false; it can only be obeyed or violated. The second is an observation, true of one run at one commit. The third is a fact about a file at a moment. The fourth is a hypothesis, and a decent one, but it is the agent's own guess and it was never checked.&lt;/p&gt;

&lt;p&gt;Write all four as notes and they become the same kind of object: a string with a timestamp. Retrieve them together later and the fourth reads exactly like the first three. Worse, the fourth is often the most useful-sounding, because it is the only one phrased as an explanation. Explanations are what a retrieval query tends to be reaching for.&lt;/p&gt;

&lt;p&gt;The problem is not that the fourth note is wrong. It might be right. The problem is that nothing in the system records that it is the kind of thing that could be wrong, while the other three are not, and they are not wrong in three different ways.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The claim, more precisely.&lt;/strong&gt; The four statements have different truth conditions, different ways of being wrong, and different correct responses on being contradicted. A human assertion that conflicts with an observation usually means the instruction was normative, not descriptive, and the right move is to ask. A tool observation that conflicts with a later observation means the world changed, and the right move is to re-run. A model conclusion that conflicts with anything is wrong, and the right move is to drop it. Collapse the three into one type and there is exactly one repair strategy, which is whatever the model guesses. That guess is itself unlabeled, and now the corruption is recursive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Provenance is not a new idea in computing. The W3C &lt;a href="https://www.w3.org/TR/prov-dm/" rel="noopener noreferrer"&gt;PROV-DM&lt;/a&gt; standard from 2013 has spent more than a decade defining how to record who or what produced a piece of data, why, and from which inputs. Scientific workflow systems like Taverna and Kepler ship lineage-aware stores that track derivation chains across tool calls. The narrower claim is in agent memory, where the failure that dominates is not the one provenance was originally designed to address, which is trust assessment. It is type destruction, which happens earlier and further upstream, at the boundary between context and store.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 2: Why It Hides
&lt;/h1&gt;

&lt;h2&gt;
  
  
  02 · Containers Carry Provenance, Content Does Not
&lt;/h2&gt;

&lt;p&gt;Inside a live context the four statements are separable, and the model separates them well. It knows the user spoke because the user's turn is a user turn. It knows the test output is tool output because it sits in a tool result block. It knows its own guess is a guess because it wrote the word "probably" three lines ago and can still see it.&lt;/p&gt;

&lt;p&gt;None of that survives extraction. Every one of those signals is carried by the container, not by the content, and memory stores content.&lt;/p&gt;

&lt;p&gt;When a memory tool writes a string to a file, the only thing in the file is the string. The fact that the string was the user's instruction rather than the model's guess is in the role it played in the conversation, and the role is gone. The fact that the string was a tool observation rather than a hypothesis is in the block it sat inside, and the block is gone. The fact that the string was a hedged guess is in a word that any downstream system is free to keep, drop, paraphrase, or replace.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Analogy.&lt;/strong&gt; Imagine a courtroom record reduced to a list of every sentence anyone said during the trial, with no speaker labels, no timestamps, and no markers for which sentences were testimony, which were objections, which were the judge's rulings. A future reader, given only that list, has to guess whether any particular sentence was a fact a witness swore to or a lawyer's speculation. The guess is the only move available, and the guess is wrong more often than not, because the speakers were not equivalent. The transcript is fine. The record has lost the structure that made it usable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is why the failure is so easy to miss in testing. Evaluate memory inside a single session and provenance appears to work, because the context still holds the structure that encodes it. The corruption only shows up across the boundary that memory exists to cross, which is exactly where it is least likely to be measured. The boundary is also the only place the system gets to demonstrate it is doing something useful, so it is the place evaluation spends the least time.&lt;/p&gt;

&lt;p&gt;There is a second reason it hides. When a laundered note is later acted on, the action usually succeeds or fails for ordinary reasons, and the failure gets attributed to the action. Nobody traces back to ask whether the belief that motivated it was ever load-bearing. The causal chain from a bad epistemic label to a bad outcome is long, and every link in it looks like a normal engineering problem. The end failure is a missed deadline, a wrong commit, a botched deploy, and the postmortem names the tool, not the memory.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 3: The Asymmetry
&lt;/h1&gt;

&lt;h2&gt;
  
  
  03 · Forgetting and Laundering Are Not Symmetric
&lt;/h2&gt;

&lt;p&gt;Treat memory as a channel with two error modes.&lt;/p&gt;

&lt;p&gt;The first is loss. A note that should have come back did not. The cost is a re-derivation: the agent reads the file again, runs the test again, asks the user again. Annoying, measurable, and bounded by the cost of the original derivation. Crucially, the agent knows it does not know. It is in a state where it will go and find out.&lt;/p&gt;

&lt;p&gt;The second is laundering. A note comes back, and it carries more authority than it earned. The cost is not bounded by anything, because the agent is no longer in a state where it will go and find out. It has an answer. Every downstream decision inherits the error, and the error is silent by construction: there is no exception, no miss, no empty result, nothing that looks like a failure at the moment it occurs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The two error modes, side by side.&lt;/strong&gt; Loss is a missing note. The agent has no information, knows it, and pays a bounded price to obtain the information. Laundering is a mislabelled note. The agent has a confident answer to something it should have hedged, builds on it, and never trips a signal that says the answer was wrong. Loss is loud and finite. Laundering is silent and unbounded, because the only cost is whatever the downstream decisions multiply it by.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These two errors are not symmetric and should not be traded off against each other as if they were. Yet almost every design decision in a memory system trades exactly that way. Store more, and you launder more. Retrieve more aggressively, and you launder more. Summarize to save tokens, and you launder more, because summarization strips hedges first, and the next section is about why.&lt;/p&gt;

&lt;p&gt;An agent that remembers nothing is merely slow. An agent that remembers its own guesses as facts is &lt;strong&gt;confidently wrong, and it will defend the position&lt;/strong&gt;, because from the inside a laundered note is indistinguishable from a thing it was told.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 4: Hedges Do Not Survive
&lt;/h1&gt;

&lt;h2&gt;
  
  
  04 · The Compression Tax on Uncertainty
&lt;/h2&gt;

&lt;p&gt;There is a specific, mechanical reason provenance decays even when someone tries to preserve it in the text.&lt;/p&gt;

&lt;p&gt;Epistemic status in natural language is carried by small, low-content words. "Probably." "I think." "It looks like." "Assuming the runner is network-bound." These words have almost no semantic weight. They contribute little to an &lt;em&gt;embedding&lt;/em&gt;. They are the first casualties of any rewrite that optimizes for brevity or clarity.&lt;/p&gt;

&lt;p&gt;So the moment a note passes through a summarizer, a compaction step, or an agent rewriting its own notes to be tidier, the hedges go and the claim stays. The sentence gets shorter and more confident at the same time, and the two are the same edit. Nobody decided to promote the guess. The promotion is a side effect of compression.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demo 01 · Run a Hedged Note Through a Summarizer.&lt;/strong&gt; Pick a note with epistemic markers and watch the markers survive or vanish as a compressor rewrites it for brevity. The same note can pass through a hedge-preserving compressor and a hedge-stripping one, and the output is a different epistemic object even though the words are nearly the same. This is the mechanism that makes laundering free, and it does not require an adversary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The published work on this drift is more recent than the intuition, and more measured than the intuition would predict. Belem, Wu, Yao, Steyvers, Singh, and Smyth, &lt;a href="https://arxiv.org/abs/2606.07951" rel="noopener noreferrer"&gt;&lt;em&gt;From "May" to "Is": Certainty Distortion in Language Model Rewriting&lt;/em&gt;&lt;/a&gt; (arXiv:2606.07951, June 2026), report a systematic tendency for LMs to overstate confidence when paraphrasing scientific and medical text. Their evaluation covers single rewrites, repeated paraphrases, and prompt-level mitigations, and the effect compounds over iterations: in their medical subset, one pass raises certainty in 20% of examples, five passes in 40%. The implication for memory stores is that the longer a note lives, and the more rewrite passes it sees, the more it tends to wear a confidence the original never had.&lt;/p&gt;

&lt;p&gt;This is why "just write good notes" does not solve it. The instruction assumes the note is stable once written. It is not. It is a string that will be passed through several lossy transformations by systems that have no reason to know which of its words are the important ones.&lt;/p&gt;

&lt;p&gt;If provenance is in the prose, provenance is negotiable. &lt;strong&gt;It has to be in the schema.&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 5: The Fix
&lt;/h1&gt;

&lt;h2&gt;
  
  
  05 · A Closed Enum, Not a Source String
&lt;/h2&gt;

&lt;p&gt;The fix is not a &lt;code&gt;source&lt;/code&gt; string. A free-text source field is prose again, and it will be paraphrased away like everything else.&lt;/p&gt;

&lt;p&gt;What is needed is a small closed set of classes, assigned at write time, that the retrieval path is required to render and the model is trained by the interface to read. Small, because a taxonomy nobody can remember is a taxonomy nobody applies correctly. Closed, because an open set collapses back into prose.&lt;/p&gt;

&lt;p&gt;The minimum viable distinction is between what a human asserted, what a tool emitted, and what the model concluded. Those three have genuinely different truth conditions, different ways of being wrong, and different correct responses on being contradicted.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provenance class&lt;/th&gt;
&lt;th&gt;Truth condition&lt;/th&gt;
&lt;th&gt;How it is wrong&lt;/th&gt;
&lt;th&gt;Correct response on conflict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;human&lt;/td&gt;
&lt;td&gt;Reflects what the user said or endorsed&lt;/td&gt;
&lt;td&gt;Misquoted, transcribed, or out of date&lt;/td&gt;
&lt;td&gt;Ask. A descriptive conflict may be normative intent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tool&lt;/td&gt;
&lt;td&gt;Reflects an observation at a moment&lt;/td&gt;
&lt;td&gt;World changed, tool was nondeterministic, or anchor drifted&lt;/td&gt;
&lt;td&gt;Re-run. The world is the source of truth.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;model&lt;/td&gt;
&lt;td&gt;Reflects the model's own inference&lt;/td&gt;
&lt;td&gt;The inference was wrong&lt;/td&gt;
&lt;td&gt;Drop. The conclusion is what is wrong.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is a further refinement worth naming, because it is where this gets genuinely hard. The middle case is a claim a human made that an agent transcribed, perhaps compressed, perhaps subtly shifted in the paraphrase. It is not a model conclusion, because a human is behind it. It is not a verbatim human assertion either, because the exact words are the agent's. Systems that offer only two classes force this case into the wrong one, and it is a common case, arguably the most common one in practice, because almost every "the user said X" note in any real store was typed by the agent that heard it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why a free-text source field is not enough.&lt;/strong&gt; A &lt;code&gt;source="user said in turn 14"&lt;/code&gt; column is the same as no source column, because the value is prose, and the retrieval path that does not know what to do with prose is exactly the retrieval path that needs the source. A closed enum, by contrast, is rendered on every recall. The reading model does not have to interpret it. It reads the prefix the way it reads the kind field on a tool call, and that is the entire mechanism. Open taxonomies collapse back to prose, and prose collapses back to nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An earlier piece, &lt;a href="https://swapnanilsaha.com/blog/agent-memory-trust-ladder/" rel="noopener noreferrer"&gt;&lt;em&gt;Agent Memory Needs a Trust Ladder&lt;/em&gt;&lt;/a&gt;, is about how much weight to give a stored note. It presupposes the system already knows what kind of note it is holding: human, agent-recorded, or auto-captured. The trust axis and the kind axis are independent, and conflating them is a common error. A trust ladder on a typeless memory is the downstream problem: the model is being asked how much to believe a claim whose type was destroyed before the question was asked. A separate but adjacent piece, &lt;a href="https://swapnanilsaha.com/blog/agents-over-trust-instructions-and-under-trust-corrections/" rel="noopener noreferrer"&gt;&lt;em&gt;Agents Over-Trust Instructions and Under-Trust Corrections&lt;/em&gt;&lt;/a&gt;, makes a related point about how instructions and corrections are weighted asymmetrically by the reading model. The instruction-versus-correction asymmetry is a second downstream consequence of the same typeless assumption. Both pieces are right about their own axis, and both assume the type is known. The kind axis sits beneath both.&lt;/p&gt;

&lt;p&gt;The published literature on this kind of labelling is mostly recent. Wang et al., &lt;a href="https://arxiv.org/abs/2606.04990" rel="noopener noreferrer"&gt;&lt;em&gt;From Agent Traces to Trust: A Survey of Evidence Tracing and Execution Provenance in LLM Agents&lt;/em&gt;&lt;/a&gt; (arXiv:2606.04990, June 2026), explicitly groups memory writes by source type, recommending that stores carry source, authoring agent, transformation operation, confidence, and update history on every memory. The taxonomy there is richer than the three-class minimum above, and a system implementing the richer version is in better shape. The survey does not separate cleanly the case where the source field exists in the schema but the retrieval path does not actually surface it to the reading model. That is a delivery problem, and it has the same shape as every other delivery problem in agent memory: the artifact is in the store and never reaches the prompt.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 6: Why Not at Read Time
&lt;/h1&gt;

&lt;h2&gt;
  
  
  06 · Information Destroyed at Write Time
&lt;/h2&gt;

&lt;p&gt;A tempting shortcut: skip the write-time labeling, and have the model assess provenance when it reads a note. It is good at that sort of judgment, in some situations.&lt;/p&gt;

&lt;p&gt;It cannot work, and the reason is worth being precise about.&lt;/p&gt;

&lt;p&gt;Provenance is not a property of the note's text. It is a property of the note's history. Two notes with byte-identical content can have different provenance, because one was dictated by a user and the other was inferred by a model that happened to phrase it the same way. No amount of reading the string recovers which happened. The information is not in the string; it was destroyed at write time, and read-time analysis has nothing to recover it from.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Demo 02 · Two Notes, Identical Strings, Different Histories.&lt;/strong&gt; Two notes in the store, byte-identical in their content. One was the user speaking in a real conversation. The other was the model inferring a guess and writing it down with a phrasing it happened to share. A read-time judge sees the same text in both cases. Toggle the framing, then toggle a read-time inference on, and watch how the inference produces a plausible-sounding answer that is the original failure, one level up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What read-time assessment actually produces is a plausible-sounding guess about provenance, stored or acted on as if it were provenance. Which is the original failure, one level up.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The general shape, named once.&lt;/strong&gt; When information is destroyed at write time, no read-time cleverness restores it. It can only manufacture a confident substitute. The fix has to be at the boundary where the information still exists. This is not a special case. It is the standard shape of a class of memory bugs, and naming it once means the rest of the family becomes recognizable: write-time compression that loses hedging, write-time rewriting that flattens a model conclusion into a "fact", write-time summarization that drops the actor on a retraction. Each of them is the same bug in a different surface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last sentence is the one to keep. Every summarization pass, every rewrite-for-clarity, every compaction step is the same shape: a write-time operation that throws away the part of the string the system cannot yet use, on the bet that the part it kept will be enough. The bet is wrong, but it is wrong silently, which is what makes the bug shape worth recognizing.&lt;/p&gt;

&lt;h1&gt;
  
  
  Part 7: Predictions
&lt;/h1&gt;

&lt;h2&gt;
  
  
  07 · What This Forbids, and the Test That Would Break It
&lt;/h2&gt;

&lt;p&gt;An argument that explains everything and forbids nothing is not worth much. Here is what this one forbids.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 1.&lt;/strong&gt; In a memory system without provenance classes, the proportion of stored notes that are model conclusions rather than observations should rise over time, and rise faster in longer sessions. Conclusions are cheap to generate and feel insightful, so an agent asked to record what it learned will preferentially record what it thought. This is measurable by classifying an existing store retrospectively. The slope should be positive and significantly larger than the slope for human-asserted or tool-observed notes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 2.&lt;/strong&gt; Notes that have been through a summarization or compaction step should be measurably more confident in phrasing than their originals, with no corresponding increase in support. Diff the hedge tokens before and after. The published work already gives a baseline: Belem et al. find certainty distortion in up to 75% of LM rewrites, with a 1.5 to 2 times asymmetry toward inflation. If a memory system shows flat hedge rates through rewriting, either the system is suppressing the drift by accident or the published result does not generalise; either outcome would weaken the argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction 3.&lt;/strong&gt; Given a memory store containing a known-wrong model conclusion, an agent should be more likely to defend and build on it than to re-derive it, compared to a control where the same claim is labeled as a guess. &lt;strong&gt;If labeling makes no behavioral difference, the schema fix is not worth building: at that point it is an aesthetic preference rather than an engineering claim.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The load-bearing prediction.&lt;/strong&gt; The reading model actually changing behavior in response to an epistemic label is the load-bearing assumption, and it deserves a direct test rather than an appeal to plausibility. It is exactly the kind of thing that could come back negative. The published literature on verbalized calibration, in particular Tian et al. 2023, has been pretty clear that LMs are poorly calibrated about their own reasoning: they overclaim correctness, underclaim uncertainty, and the calibration error is large enough that a self-reported confidence stamp on a recalled note may add noise rather than signal. If that is true, the labelling scheme has to be enforced at the rendering layer, not the reading layer, and the schema is the right place to enforce it. The test that would settle it is the third prediction above, run with a real store and a real agent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things the argument does not claim. It does not claim that labelling alone makes an agent correct. A labelled-and-laundered note is still wrong, just labelled as wrong with the wrong label. It does not claim that provenance classes are a security mechanism. A malicious writer can attach a &lt;code&gt;human&lt;/code&gt; stamp to anything it can write, and a closed enum is no defense against a hostile actor with direct store access. The argument is about accidental corruption, not adversarial input, and treating the two as one would be a category mistake.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Line to Keep
&lt;/h1&gt;

&lt;p&gt;An agent that forgets is slow. An agent that cannot tell what it was told from what it guessed is confidently wrong, and it will defend the position. Forgetting is safe. Laundering is not.&lt;/p&gt;

&lt;p&gt;The fix is small. A closed enum, stamped at write, rendered on every recall, with the summarization pipeline forbidden from rewriting it. Three classes are the minimum. The argument does not need anything else. The argument does need that one thing, because without it every note the system stores is a free-floating claim with no information about how it came to exist, and the most plausible-sounding notes are the ones that came from the worst path.&lt;/p&gt;

&lt;h1&gt;
  
  
  Sources
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;W3C Working Group. &lt;a href="https://www.w3.org/TR/prov-dm/" rel="noopener noreferrer"&gt;PROV-DM: The PROV Data Model&lt;/a&gt;. W3C Recommendation, 30 April 2013. The standard conceptual model for provenance on the web: entities, activities, agents, plus seven binary relations including WasDerivedFrom and WasAttributedTo. Part 1.&lt;/li&gt;
&lt;li&gt;Wang, Y., Zhang, J., Cai, T., Liu, Z., Sun, Q., Sun, Z., Wu, Z., Dong, M., Zheng, M., Yin, X., Zhu, Y. &lt;a href="https://arxiv.org/abs/2606.04990" rel="noopener noreferrer"&gt;From Agent Traces to Trust: A Survey of Evidence Tracing and Execution Provenance in LLM Agents&lt;/a&gt;. arXiv:2606.04990, June 2026. Organises memory writes by source type and recommends a per-memory provenance field with source, actor, transformation, confidence, and update history. Part 5.&lt;/li&gt;
&lt;li&gt;Belem, C. G., Wu, S., Yao, H., Steyvers, M., Singh, S., Smyth, P. &lt;a href="https://arxiv.org/abs/2606.07951" rel="noopener noreferrer"&gt;From "May" to "Is": Certainty Distortion in Language Model Rewriting&lt;/a&gt;. arXiv:2606.07951, June 2026. Measures certainty drift in up to 75% of LM rewrites, with a 1.5 to 2x asymmetry toward inflation, and a compounding effect over repeated paraphrasing. The published evidence behind Part 4's mechanism.&lt;/li&gt;
&lt;li&gt;Chhikara, P., Khant, D., Aryan, S., Singh, T., Yadav, D. &lt;a href="https://arxiv.org/abs/2504.19413" rel="noopener noreferrer"&gt;Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory&lt;/a&gt;. arXiv:2504.19413, April 2025. Update phase uses ADD, UPDATE, DELETE, NOOP operations with no per-note provenance class. A canonical example of the typeless-store pattern. Part 1.&lt;/li&gt;
&lt;li&gt;Packer, C., Wooders, S., Lin, K., Fang, V., Patil, S. G., Stoica, I., Gonzalez, J. E. &lt;a href="https://arxiv.org/abs/2310.08560" rel="noopener noreferrer"&gt;MemGPT: Towards LLMs as Operating Systems&lt;/a&gt;. arXiv:2310.08560, October 2023. The two-tier core/archival memory model that is the de facto reference for the field. No provenance class is rendered on recall; stored as a counterexample. Part 1.&lt;/li&gt;
&lt;li&gt;Tian, K., Mitchell, E., Zhou, A., Sharma, A., Rafailov, R., Yao, H., Finn, C., Manning, C. D. &lt;a href="https://arxiv.org/abs/2305.14975" rel="noopener noreferrer"&gt;Just Ask for Calibration: Strategies for Eliciting Calibrated Confidence Scores from Language Models Fine-Tuned with Human Feedback&lt;/a&gt;. arXiv:2305.14975, May 2023, EMNLP 2023. Shows RLHF-tuned LMs can verbalize better-calibrated confidences than their token probabilities, but the calibration error remains large enough that a self-reported confidence stamp is not a substitute for schema-level provenance. Part 7.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Related reading
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/agent-memory-trust-ladder/" rel="noopener noreferrer"&gt;Agent Memory Needs a Trust Ladder&lt;/a&gt;: The layer above: once provenance is preserved, how much weight to give each kind of note. The trust axis is the weight question. The kind axis is the type question, and naming the distinction is the point of linking them.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/agents-over-trust-instructions-and-under-trust-corrections/" rel="noopener noreferrer"&gt;Agents Over-Trust Instructions and Under-Trust Corrections&lt;/a&gt;: The adjacent finding: even when the type is known, agents weight instructions and corrections asymmetrically. A second downstream consequence of the same typeless assumption, with a different shape of failure.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://swapnanilsaha.com/blog/reinstatement-is-the-missing-primitive/" rel="noopener noreferrer"&gt;Reinstatement Is the Missing Primitive in Agent Memory&lt;/a&gt;: The downstream effect after a wrong note is found: how to make revocation usable. Belongs one level below provenance: provenance is what tells you a note is a guess, reinstatement is what undoes a wrong revocation of one.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agentmemory</category>
      <category>epistemics</category>
      <category>llmagents</category>
      <category>developertools</category>
    </item>
  </channel>
</rss>
