<?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: Harrison Guo</title>
    <description>The latest articles on DEV Community by Harrison Guo (@harrisonsec).</description>
    <link>https://dev.to/harrisonsec</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%2F3809272%2F593698c5-7201-4bb0-898e-055cdbc0a2d2.png</url>
      <title>DEV Community: Harrison Guo</title>
      <link>https://dev.to/harrisonsec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harrisonsec"/>
    <language>en</language>
    <item>
      <title>A Wrong Ruler Is Worse Than No Ruler: Verifying the Checks You Trust</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 19:47:14 +0000</pubDate>
      <link>https://dev.to/harrisonsec/a-wrong-ruler-is-worse-than-no-ruler-verifying-the-checks-you-trust-1gkb</link>
      <guid>https://dev.to/harrisonsec/a-wrong-ruler-is-worse-than-no-ruler-verifying-the-checks-you-trust-1gkb</guid>
      <description>&lt;p&gt;There is one failure mode I have learned to fear more than a missing check.&lt;/p&gt;

&lt;p&gt;A system with no check for something is at least honest about it. The gap is visible, the uncertainty is real, and everyone downstream knows not to lean on that part. A system with a &lt;em&gt;wrong&lt;/em&gt; check for the same thing is worse, and it is worse in a specific, dangerous way. It answers with confidence. It overrules the signals that were actually right. And it does all of this wearing the one badge nobody thinks to question. &lt;em&gt;It's deterministic, so it must be correct.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A wrong check with authority is worse than no check at all. No check leaves you honestly uncertain. A wrong authoritative check leaves you confidently wrong, and it silences the correct signal it overruled.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The two companion pieces to this one both push in the same direction: &lt;a href="https://harrisonsec.com/blog/shrink-the-stochastic-surface/" rel="noopener noreferrer"&gt;put as much of the system as you can onto deterministic ground&lt;/a&gt;, and &lt;a href="https://harrisonsec.com/blog/the-technique-boundary/" rel="noopener noreferrer"&gt;let a rule decide wherever a rule can&lt;/a&gt;. I still believe every word of that. But both standards make a quiet assumption I want to drag into the light. When they say &lt;em&gt;rules first&lt;/em&gt;, and especially when they say a deterministic rule may &lt;em&gt;overrule&lt;/em&gt; a probabilistic judge, they assume the rule is right. This piece is about what happens when it is not, and how to make sure it is before you let it hold a veto.&lt;/p&gt;

&lt;h2&gt;
  
  
  The badge nobody checks
&lt;/h2&gt;

&lt;p&gt;A deterministic number reads as ground truth. That is its great virtue and its hidden trap. When a metric prints &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;47&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, it does not sound like an opinion, it sounds like a fact, and we grant it the standing of one. In a rules-first architecture we go further: we give the deterministic layer &lt;em&gt;authority over the probabilistic one&lt;/em&gt;. The rule gates the output. The rule vetoes the judge. That is the correct design. A fluent model that hallucinates a verdict should not get to overrule a schema check that a parser can settle.&lt;/p&gt;

&lt;p&gt;But look at what that authority does to the cost of an error. An advisory signal that is wrong is noise: you can ignore it. An &lt;em&gt;authoritative&lt;/em&gt; signal that is wrong is not noise, it is a wrong answer with the power to enforce itself. And the badge of determinism, the quiet "it can't be wrong, it's just arithmetic," is exactly what stops anyone from auditing it. The check you trust the most, because it is deterministic and it is yours, is the one whose errors you are least equipped to catch. You built it to be believed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A metric that lied
&lt;/h2&gt;

&lt;p&gt;Here is the one that taught me the lesson.&lt;/p&gt;

&lt;p&gt;Picture an evaluator for generated long-form documents, the kind a model drafts section by section. One of its quality checks looks for lazy repetition: a document that just restates the same paragraph a dozen times over is worse than one that actually develops. A reasonable thing to measure. The implementation folded each paragraph into a signature, a hash of its normalized text, and flagged the document when too many signatures came out identical.&lt;/p&gt;

&lt;p&gt;The hash was cheap, and it was broken. It collided, so genuinely different paragraphs sometimes produced the same signature. And it was brittle to trivial edits, so the same paragraph with two words swapped or its sentences reordered hashed to something new. The net effect was a metric that missed near-duplicates when their surface form changed, and occasionally invented sameness where the content was genuinely different.&lt;/p&gt;

&lt;p&gt;On its own, a buggy metric is just noise you could learn to ignore. But this one had been handed authority. When the model judge read the document and said, correctly, "these sections are near-duplicates of each other," the repetition metric was allowed to &lt;em&gt;veto&lt;/em&gt; the complaint: "the signatures don't match, so the judge is imagining it." And the veto won. The score came out clean.&lt;/p&gt;

&lt;p&gt;Sit with what that pipeline was doing on that facet. It was running its own logic backwards: using a &lt;em&gt;wrong deterministic signal&lt;/em&gt; to overrule a &lt;em&gt;correct subjective one&lt;/em&gt;, and then reporting a better score for the trouble. The judge had been right. The rule silenced it. And because the silencing came from a deterministic check, it arrived stamped with confidence, and every inflated score looked earned. Nobody goes back to re-litigate a number the arithmetic already settled. That is the trap closing.&lt;/p&gt;

&lt;p&gt;The failure did not surface as an error. It surfaced as &lt;em&gt;cleaner results&lt;/em&gt;, which is the most dangerous shape a failure can take, because clean results are what everyone was hoping for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why wrong-and-authoritative is the worst quadrant
&lt;/h2&gt;

&lt;p&gt;Lay it out as a two-by-two: a check is either right or wrong, and it either carries authority or is merely advisory. Three of those cells are fine. A right check with authority is the whole point of the discipline. A right check that is only advisory is a mild waste. A wrong check that is only advisory is ignorable noise. It is the fourth cell, &lt;strong&gt;wrong and authoritative&lt;/strong&gt;, that is not a lesser version of a good check but an active liability, and it is worse than the empty cell where no check exists at all.&lt;/p&gt;

&lt;p&gt;Compare the two directly. With no repetition check, you know you cannot currently measure repetition. The gap is on the map, and a repetitive document simply passes unremarked, the same as anything else you haven't instrumented. With a &lt;em&gt;wrong&lt;/em&gt; repetition check that vetoes the judge, a repetitive document passes &lt;em&gt;and&lt;/em&gt; the one signal that correctly caught it gets overruled &lt;em&gt;and&lt;/em&gt; the final score ticks upward, all under a badge that discourages anyone from doubting it. The missing check costs you a blind spot you know about. The wrong check costs you the correct answer you already had, plus the false confidence that you don't need to look.&lt;/p&gt;

&lt;p&gt;Which yields the single most useful habit I took from this: &lt;strong&gt;suspect the metric first.&lt;/strong&gt; When a deterministic check refutes an observation that a careful human or a competent model would make, the base rate is not on the check's side. A hand-rolled hash is far more likely to be broken than a fluent reader is to be hallucinating "these sections are near-duplicates" about a document where they visibly are. The reflex to trust the number &lt;em&gt;because it is a number&lt;/em&gt; is precisely the reflex that keeps the wrong number in charge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other way a ruler lies
&lt;/h2&gt;

&lt;p&gt;There is a second ruler that lies, and it lies in the very layer the first one was busy overruling. It is the bare absolute score: &lt;code&gt;quality = 0.6&lt;/code&gt;, emitted by a model judge with no anchors, no comparison, no zero-point, and then handled as if it were a measurement.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;0.6&lt;/code&gt; like that is not a reading; it is a decimal in the costume of one. Ask what it is 0.6 &lt;em&gt;of&lt;/em&gt;. Better than which example, worse than which, on a scale pinned to what? There is no answer underneath. Humans and models share a specific profile here: both are reliable at &lt;em&gt;ordering&lt;/em&gt; ("A is closer to the brief than B") and unreliable at &lt;em&gt;absolute calibration&lt;/em&gt; ("this deserves a 0.6"). A standalone perceptual number inherits all of the unreliable half and none of the reliable half. It is a subjective opinion that has been rounded to two decimal places and thereby laundered into looking objective, the same laundering the broken hash performed, run in the other layer.&lt;/p&gt;

&lt;p&gt;The fix rhymes with the fix for the first ruler: do not trust the number until the number has earned trust. For a subjective axis, that means &lt;strong&gt;score by comparison, against anchors&lt;/strong&gt;, a set of labelled reference examples that fix what good and excellent actually look like, and rank new outputs against them instead of emitting a free decimal. And before you trust a judge's agreement with humans, measure how well humans agree with &lt;em&gt;each other&lt;/em&gt;. That inter-rater ceiling is the best score any judge can honestly aspire to. If two careful reviewers only agree seven times in ten on an axis, a judge that reports a confident number above that is not beating the humans, it is hiding the disagreement they honestly surfaced. Reporting a judge's number without that ceiling is one more measurement with no zero-point. A ruler with no marked zero and no fixed unit is not a strict ruler; it is a confident guess holding a straightedge.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a check earns its authority
&lt;/h2&gt;

&lt;p&gt;So the discipline here is not "add more checks." It is narrower and more demanding: &lt;strong&gt;before any check is allowed to gate output or overrule another signal, verify the check itself, as hard as the claim it will silence.&lt;/strong&gt; In practice that is four moves.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recompute it a second, independent way.&lt;/strong&gt; Derive the same quantity by a different method and require the two to agree. If a repetition score can be computed by hashing and also by direct comparison, do both; a disagreement is a bug in one of them, and you want to find that in a test, not discover it as a mysteriously clean production score.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Regression-test it in both directions.&lt;/strong&gt; A case you know is repetitive must trip it; a case you know is varied must not. A metric validated only in the direction it usually fires is half-tested, and the untested half is exactly where a false veto hides. And wrong does not only mean buggy. A check with correct arithmetic and a badly chosen threshold is wrong in the same way and hides in the same untested direction, so the cases you pick have to pin the boundary, not just the obvious middle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run it in shadow before it gates.&lt;/strong&gt; Put it in production computing its verdicts, but let it change nothing. Watch it at scale, side by side with the signal it will eventually overrule. A wrong metric announces itself here, as a steady stream of vetoes against observations that were plainly correct, and it does so &lt;em&gt;before&lt;/em&gt; it has moved a single real verdict. Shadow-first is not caution for its own sake; it is the one place a confidently-wrong check reveals itself while the damage is still zero.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat a veto as a claim, not a license.&lt;/strong&gt; When a rule overrules a judge, it is asserting something checkable, "these sections are not actually duplicates," and that assertion has to be as verifiable as the judge's was. The fact-check does not get to be the one unaudited step in a pipeline whose entire purpose is to audit everything else.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is an asymmetry worth naming here, because it is the whole reason this failure survives. We already do all four of these things for models. We stage a new model behind a flag, we hold a judge back until it agrees with humans, we assume the probabilistic thing needs proving before we lean on it. We almost never extend the same suspicion to a rule, because the rule wears the badge that says it does not need it. That asymmetry is the bug. The deterministic layer is handed the most authority and subjected to the least verification, and it is handed that authority &lt;em&gt;precisely because&lt;/em&gt; nobody expects to have to check it.&lt;/p&gt;

&lt;p&gt;Underneath all four is a habit good teams already apply to &lt;em&gt;other people's&lt;/em&gt; numbers. When a result comes back too clean, too uniform, suspiciously convenient, you don't celebrate it. You go verify it against the source before you repeat it. This piece is that same skepticism, turned inward, and pointed most sharply at whatever metric you have handed a gate or a veto. External results earn scrutiny because they might be flattering. Your own authoritative checks deserve &lt;em&gt;more&lt;/em&gt;, because they are flattering and you trust them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a rough check is fine
&lt;/h2&gt;

&lt;p&gt;Every standard needs its boundary, so here is this one's: &lt;strong&gt;the danger is authority, not error.&lt;/strong&gt; A rough, sometimes-wrong signal is not merely tolerable, it is useful, as long as it stays advisory. A heuristic that flags documents for a human to glance at, a cheap metric that sorts a review queue, a smell test that nudges attention toward the likely problems: each can be wrong a fair fraction of the time and still pay for itself, because the cost of a wrong advisory signal is a wasted glance.&lt;/p&gt;

&lt;p&gt;The line is crossed the moment a metric gains authority, the moment it can fail a build, block an output, or overrule another signal without appeal. At that instant its tolerance for being wrong drops toward zero, because its errors stop costing a glance and start costing the correct signal it now outranks. So the standard is not "verify every check to perfection," which would freeze you. It is "match the verification to the authority." Advisory signals can be rough and useful. Anything holding a veto has to have earned it, in both directions, in the open, before it ever changes a verdict.&lt;/p&gt;

&lt;p&gt;A missing ruler tells you honestly that you cannot measure something yet. A wrong ruler tells you confidently that you can, and hands you the wrong number with a straight face. And if that ruler also has the authority to overrule the one instrument that was reading correctly, it will quietly make your whole system worse while every dashboard turns greener. Between the two, the honest gap is the safer place to stand. The only work worth doing from there is turning it into a ruler you have actually checked.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This piece extends two companions. The standard it builds on: &lt;a href="https://harrisonsec.com/blog/shrink-the-stochastic-surface/" rel="noopener noreferrer"&gt;Shrink the Stochastic Surface&lt;/a&gt;. The boundary it assumes: &lt;a href="https://harrisonsec.com/blog/the-technique-boundary/" rel="noopener noreferrer"&gt;Determinism Where You Can, Judgement Where You Must&lt;/a&gt;. And the architecture both sit inside: &lt;a href="https://harrisonsec.com/blog/generative-ai-builds-shapes-not-games/" rel="noopener noreferrer"&gt;Generative AI Builds Shapes, Not Games&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>programming</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Determinism Where You Can, Judgement Where You Must: The Technique Boundary for AI Systems</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Thu, 09 Jul 2026 15:27:05 +0000</pubDate>
      <link>https://dev.to/harrisonsec/determinism-where-you-can-judgement-where-you-must-the-technique-boundary-for-ai-systems-1o4h</link>
      <guid>https://dev.to/harrisonsec/determinism-where-you-can-judgement-where-you-must-the-technique-boundary-for-ai-systems-1o4h</guid>
      <description>&lt;p&gt;I have now written this same sentence three times in three pieces, so it is time to write the sentence underneath it.&lt;/p&gt;

&lt;p&gt;The first companion piece, &lt;a href="https://harrisonsec.com/blog/generative-ai-builds-shapes-not-games/" rel="noopener noreferrer"&gt;Generative AI Builds Shapes, Not Games&lt;/a&gt;, argued that generative AI produces plausible shapes and that correctness has to come from structure and verification wrapped around the generator. That answers &lt;em&gt;whether&lt;/em&gt; you need something outside the model. The second, &lt;a href="https://harrisonsec.com/blog/shrink-the-stochastic-surface/" rel="noopener noreferrer"&gt;Shrink the Stochastic Surface&lt;/a&gt;, argued that reliability is set by how much of the output you leave the model solely responsible for, and that the work is shrinking that fraction. That answers &lt;em&gt;how much&lt;/em&gt; structure, and &lt;em&gt;where the line goes&lt;/em&gt; between model and machine.&lt;/p&gt;

&lt;p&gt;Both leave one question open. Once you have decided that a part of the system should be deterministic structure rather than a raw sample, &lt;em&gt;which&lt;/em&gt; structure? A rule? A state machine? A model reasoning inside a harness? And the question almost everyone is getting wrong in 2026: should any of it be an agent?&lt;/p&gt;

&lt;p&gt;This piece is about that boundary. It is less glamorous than the other two because it is the part you have to get right in the code, not on the whiteboard, but it is where systems actually live or die. Here is the law, as plainly as I can put it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Determinism where you can, probabilistic judgement only where the question genuinely demands it, and control always with the orchestrator, never with an autonomous loop. The model may reason under that control; it may not direct the control itself.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything below is an unpacking of that one sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four techniques, not one tool
&lt;/h2&gt;

&lt;p&gt;The dominant failure mode in AI system design right now is that everything collapses into a single verb: &lt;em&gt;call the model&lt;/em&gt;. Need to validate something, call the model. Need to route, call the model. Need to decide what to do next, call the model. It feels modern and it is almost always wrong, because it treats four fundamentally different kinds of computation as if they were one.&lt;/p&gt;

&lt;p&gt;There are four techniques, and each has exactly one correct home.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A rule engine&lt;/strong&gt; owns any question with one machine-computable, reproducible answer. Is this valid syntax? Is the payload under the size bound? Does this reference resolve? Is the structure connected? These are not opinions. A parser answers them in microseconds, for free, and is right every single time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A state machine&lt;/strong&gt; owns lifecycle and flow, the questions where &lt;em&gt;what happens next&lt;/em&gt; depends on &lt;em&gt;where you are now&lt;/em&gt; and only some transitions are legal. A job goes &lt;code&gt;pending → running → done | failed | timeout&lt;/code&gt;. A retry goes &lt;code&gt;closed → open → half-open&lt;/code&gt;. The point of making this explicit is that it kills an entire bug class, the one you get from ad-hoc boolean flags, where a job somehow transitions from &lt;code&gt;completed&lt;/code&gt; back to &lt;code&gt;running&lt;/code&gt; because two flags disagreed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An LLM&lt;/strong&gt; owns probabilistic judgement, and only that: the questions that are genuinely subjective and that no rule can answer. Is this writing coherent? Does this read as the thing the prompt asked for? Is it good versus merely acceptable? There is no formula for "cozy." Something has to make a perceptual call, and for the parts a rule cannot reach, that something is a model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An agent&lt;/strong&gt;, an LLM in a loop choosing its own next actions, owns far less than the current enthusiasm suggests, and in a large class of systems it owns nothing at all. We will get to why.&lt;/p&gt;

&lt;p&gt;The reason the boundary matters is that placing work in the wrong tier is expensive in &lt;em&gt;both&lt;/em&gt; directions, and people usually only notice one.&lt;/p&gt;

&lt;p&gt;Put an LLM where a rule belongs and you get the obvious costs, slower and more expensive, plus a subtle one that is worse: it is &lt;em&gt;less correct&lt;/em&gt;. An LLM can hallucinate a syntax verdict. A parser cannot. You have taken a question with a perfect deterministic answer and handed it to the one tool in your kit that can be confidently wrong about it. That is not a trade of accuracy for flexibility. It is strictly worse on every axis.&lt;/p&gt;

&lt;p&gt;Put a rule where the question is genuinely subjective and you get the opposite failure: the rule simply cannot answer, so you either ship a gate that misses the thing it exists to catch, or you contort a threshold into pretending a taste question is arithmetic. Both are ways of lying about the nature of the problem.&lt;/p&gt;

&lt;p&gt;Getting the boundary right is not a matter of preference. It follows from what each tool is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test that draws the line
&lt;/h2&gt;

&lt;p&gt;There is a single question that sorts most work into the right tier, and it is worth memorizing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Would two informed people always reach the same answer from the data alone?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run any criterion through it and one of three things happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yes, always.&lt;/strong&gt; The answer is a mechanical fact: a count, a dimension, a material, a connectivity check. This is the rule engine's territory, no exceptions. If two experts with the raw data in front of them cannot disagree, there is nothing for a model to add except cost and the possibility of error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not from the raw data, but the criteria can be articulated.&lt;/strong&gt; Two reviewers might land in slightly different places, but they can &lt;em&gt;explain&lt;/em&gt; what they are looking at, and that explanation can be written down as an explicit standard. This is the model's territory, but with a crucial constraint: the model's job is not ineffable taste, it is &lt;em&gt;reasoning over that written standard&lt;/em&gt;, and the reasoning is the product. A judgement that hangs on a named criterion is auditable and improvable. A bare number is neither.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The judgement is real, but nobody has written it down yet.&lt;/strong&gt; The reviewer knows excellent when they see it and cannot articulate why. This is the bucket everyone skips, and skipping it is the most common way an LLM judge silently fails. If you hand this straight to a model, it does not decline to answer. It scores against its own generic, unstated taste, and you get something reliable against the wrong ruler. The work here is &lt;em&gt;elicitation&lt;/em&gt;: dragging the tacit standard out of the domain owner and into explicit criteria, at which point it becomes the second bucket and a model can apply it. Until that happens, the honest move is to route it to a human and log what you could not yet express, not to let the model paper over the gap.&lt;/p&gt;

&lt;p&gt;The three buckets are not academic. They are the difference between a judge that applies &lt;em&gt;your&lt;/em&gt; standard and a judge that applies &lt;em&gt;its&lt;/em&gt; standard while wearing your logo. Most of the effort in building a trustworthy judge goes into the third bucket, the elicitation, and almost none of the discourse does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line that actually matters: control versus reasoning
&lt;/h2&gt;

&lt;p&gt;Now the hard part, the one the whole law turns on.&lt;/p&gt;

&lt;p&gt;The instinct, once you have accepted "determinism where you can," is to read the LLM tier as &lt;em&gt;narrow&lt;/em&gt;: one call, one score, get in and get out. That is the wrong lesson, and it leaves enormous value on the table. The boundary is not about limiting how much the model &lt;em&gt;reasons&lt;/em&gt;. A model can reason at length, chain several steps, weigh evidence, propose a plan, and all of that can be perfectly sound engineering. The boundary is about who owns &lt;em&gt;control&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Reasoning is the model producing a judgement, a diagnosis, a plan, a score, as a step inside a flow that something else drives. Control is deciding &lt;em&gt;what step happens next&lt;/em&gt; and &lt;em&gt;when the work is done&lt;/em&gt;. An agent is precisely a model that has been handed control: it chooses its own actions, re-runs stages at will, and self-determines completion. That single property, self-direction of control, is the thing to withhold.&lt;/p&gt;

&lt;p&gt;Say it as a rule: &lt;strong&gt;let the model propose, keep the disposing with the orchestrator.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fine: the model emits a diagnosis, a suggested fix, and a confidence. The orchestrator decides whether to apply it, retry, or escalate.&lt;/li&gt;
&lt;li&gt;Fine: the model reasons through five criteria and returns a structured verdict for each.&lt;/li&gt;
&lt;li&gt;Not fine: the model decides which criteria to check, re-runs whichever stages it feels like, and announces on its own that it is finished.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cleanest place this distinction shows up is function calling, because the same API serves both sides of the line. Using a tool schema to make the model &lt;em&gt;emit a structured result&lt;/em&gt;, to fill a form with a sub-score and the fact it relied on, is just an output format, and it is fine. Letting the model &lt;em&gt;choose which tools to call, in what order, and when to stop&lt;/em&gt; is a control loop, and that is what makes it an agent. Tool schemas appearing in your code tells you nothing. Who owns the loop tells you everything.&lt;/p&gt;

&lt;p&gt;Here is the part that makes the whole thing click, and it is an argument from engineering, not from taste. &lt;strong&gt;Handing control to a model is only worth it when choosing the next step actually requires intelligence.&lt;/strong&gt; In a surprising amount of real work, it does not. If the list of things to check is fixed, and each check is a static, design-time mapping from criterion to computation, then there is nothing to &lt;em&gt;decide&lt;/em&gt;. The control flow carries no intelligence. Handing it to the model in that case buys you exactly nothing and costs you a great deal: non-reproducibility, latency, token spend, and a fresh set of failure modes, loops that never terminate, checks silently skipped, tools hallucinated. You pay the full price of an agent for a control flow a &lt;code&gt;for&lt;/code&gt; loop expresses perfectly.&lt;/p&gt;

&lt;p&gt;So the question is never "could this be an agent?" Almost anything &lt;em&gt;could&lt;/em&gt; be. The question is "does choosing the next step here require judgement that is not known at design time?" Reserve autonomy for the cases where the answer is genuinely yes: open-ended or branching work whose shape you cannot lay out in advance, or an exploration phase whose entire purpose is to &lt;em&gt;discover&lt;/em&gt; the steps, which you then harden into a fixed pipeline. Everywhere else, a fixed loop with a model reasoning inside it is not a compromise. It is the better design on its own merits, before you even invoke reproducibility.&lt;/p&gt;

&lt;p&gt;There is one seam worth leaving open, because it is the place agency earns its keep even inside a fixed pipeline: the unexpected. If the model, while judging, notices something worth checking that is not on the static list, let it &lt;em&gt;propose&lt;/em&gt; an extra check. The orchestrator still decides whether to run it. That places the model's intelligence where it actually helps, noticing the unanticipated, and keeps it away from the part that needs none, selecting among known checks. Bounded, and still not driving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same boundary on the generation side
&lt;/h2&gt;

&lt;p&gt;Everything so far has been about inspection: taking an output and deciding whether it passes. But this is not an evaluation-only law. It governs generation just as strictly, and the mistake there is the mirror image. The default way to make a model produce an artifact is to ask it for the whole thing from a blank canvas, which hands the entire construction to the sampler and maximizes exactly the surface the companion pieces warned about.&lt;/p&gt;

&lt;p&gt;The disciplined version applies the same split. Let the model do the subjective, creative part, planning, choosing, proposing parameters, and let a deterministic layer execute that plan against a pre-validated structure. A model that plans a bounded edit to a known-good template, with a rule engine applying that edit inside the template's constraints, inherits the template's validity and its taste, and confines the model to a change small enough to stay valid. The invalid result becomes unreachable instead of caught downstream, which is the strongest form of the funnel from &lt;a href="https://harrisonsec.com/blog/shrink-the-stochastic-surface/" rel="noopener noreferrer"&gt;Shrink the Stochastic Surface&lt;/a&gt;: the cheapest possible check is making the wrong output impossible to emit. Same boundary, same reason. The model proposes, deterministic structure disposes, whether the thing being produced is a verdict or an artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case that makes it obvious: never put an agent in the gate
&lt;/h2&gt;

&lt;p&gt;If you want a single place to feel this law in your gut, look at evaluation.&lt;/p&gt;

&lt;p&gt;An evaluator, a quality gate, a validator, anything whose job is to render a verdict, has one non-negotiable property: &lt;strong&gt;same input, same verdict.&lt;/strong&gt; Reproducibility is not a nice-to-have for a gate; it is the whole basis of trust in it. A gate that rejects someone's work with &lt;code&gt;0.42&lt;/code&gt; today and &lt;code&gt;0.71&lt;/code&gt; tomorrow for the same input is unappealable, undebuggable, and uncalibratable. Nobody can act on it, including you.&lt;/p&gt;

&lt;p&gt;Now notice what an agent &lt;em&gt;is&lt;/em&gt;: a thing whose defining virtue is self-directed adaptivity, whose whole selling point is that it decides for itself what to do next based on what it sees. That is a wonderful property for an explorer and a catastrophic one for a ruler. An evaluator that decides for itself what to check next is non-reproducible by construction. Its adaptivity, the exact feature you would be buying it for, is the thing that destroys its authority.&lt;/p&gt;

&lt;p&gt;This is the irony of the current moment. The industry is racing to make everything agentic, and the single most seductive place to reach for an agent, "just let the smart model look at the output and judge it," is one of the places it is most wrong. The correct relationship between an agent and a gate is not that the gate is an agent. It is that the gate &lt;em&gt;watches&lt;/em&gt; the agent. Measure the agent's behaviour, count its convergence rounds, score its output, but the thing doing the scoring must be the opposite of an agent: fixed, predictable, the same ruler every time. You do not want your ruler to have opinions about how long it is today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where determinism and judgement collaborate: the rule as lie detector
&lt;/h2&gt;

&lt;p&gt;The law is not "rules and models live in separate rooms." Their most valuable interaction is when they work on the &lt;em&gt;same&lt;/em&gt; judgement, and it is the mechanism that turns an LLM judge from a black box into something auditable.&lt;/p&gt;

&lt;p&gt;Start from the honest position: the subjective part is irreducible. No rule computes "harmonious" or "coherent." A model has to produce that number. But producing the number is not the same as being trusted for it, and this is where the rule engine earns a second job.&lt;/p&gt;

&lt;p&gt;When the model scores, make it &lt;em&gt;state the evidence it relied on&lt;/em&gt;. Not just "this summary is inaccurate, 0.3," but "this summary is inaccurate because it claims the report recommends option B." Now that stated evidence often reduces to a checkable fact, and a deterministic check verifies it. Does the source document actually recommend option B, or does a string search show it never mentions B at all? Does the code the model called unsafe actually reach the unchecked path, or does a static check show the guard is present? Does the answer it praised for citing three sources actually cite three, or is it one repeated? Whenever the model's load-bearing evidence is a fact, the rule engine confirms or refutes it, and a score built on a refuted fact is voided.&lt;/p&gt;

&lt;p&gt;The division of authority is precise, and neither side can do the other's job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;model has the scoring power&lt;/strong&gt;. It produces the perceptual judgement and names the fact it rested on. The rule engine cannot author a quality score, because "harmonious" is not computable, so it never scores.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;rule engine has veto power over the evidence&lt;/strong&gt;. It verifies the cited fact and can void a score built on a false one, but it cannot replace that score with a number of its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So neither is solely decisive. When the cited fact checks out, the model's score stands. When it is refuted, the rule does not overwrite the score, it &lt;em&gt;invalidates&lt;/em&gt; it and routes to a bounded re-judge or a human. The judge becomes trustworthy not because you have decided to trust it, but because its evidence can be deterministically refuted. That is the difference between "we believe the model" and "the model cannot lie about the parts that are checkable." Only the second one is engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like assembled
&lt;/h2&gt;

&lt;p&gt;None of these techniques is interesting alone. The value is in how they compose, and the composition has a shape: a deterministic spine that owns the loop, with the model hanging off it as a called reasoning node that proposes and never drives.&lt;/p&gt;

&lt;p&gt;Read one concrete gate end to end. Content arrives. Cheap rules run first, syntax and schema and size, and anything that fails dies immediately for free. What survives enters a lifecycle state machine that governs the run. The orchestrator then calls the model as a judge: it reasons over the standard and returns per-criterion sub-scores plus the facts it relied on. Those facts go straight to a rule fact-check, which refutes any that are false and sends the judgement back for a bounded re-score. Only what clears both the rules and the verification becomes a verdict.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deterministic spine — the orchestrator owns the loop:

  content ─▶ cheap rules ─pass─▶ lifecycle FSM ─▶ [call judge] ─▶ rule fact-check ─confirmed─▶ verdict
               └─ fail fast ──────────────────────────────────────────────────────────────────▶ verdict

The judge is called and returns — it never drives:

  [call judge]  ──request──▶  LLM judge · reasoning node
                             (proposes sub-scores, cites facts; no control, no loop)
  LLM judge  ──proposal + cited facts──▶  rule fact-check
  rule fact-check  ──refuted──▶  bounded re-score  ⟲ back to [call judge]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what never happens in that picture: the model never holds the arrow that decides what runs next. It is called, it reasons, it returns. Every solid line, the loop itself, stays with the orchestrator. The dotted lines are the model's entire role, a request out and a proposal back. That is the whole law in one diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ordering falls out for free
&lt;/h2&gt;

&lt;p&gt;One more thing drops out of the boundary without any extra design, and it is the reason the whole arrangement is also the cheap one.&lt;/p&gt;

&lt;p&gt;Deterministic checks cost microseconds and no money. Model calls cost seconds and real dollars. So the boundary hands you the correct ordering by default: run the cheap, reproducible gates &lt;em&gt;first&lt;/em&gt;. They weed out the obviously broken before any paid work happens. The expensive judge runs &lt;em&gt;last&lt;/em&gt;, only on content that already cleared everything cheap. You never spend a model call scoring something that fails a syntax check, which would be burning money to grade garbage.&lt;/p&gt;

&lt;p&gt;Cost then scales with quality instead of volume: bad inputs die cheap at the front, and you only pay the expensive price for things good enough to deserve it. This is the same funnel shape the stochastic-surface piece described for reliability, and it is not a coincidence that the reliable arrangement and the cheap arrangement are the same arrangement. They are both consequences of putting each question in the tier that fits it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserve the seams, so reasoning stays additive
&lt;/h2&gt;

&lt;p&gt;The last clause of the law, control never to the agent, is the one most likely to be misread as "never add model reasoning to the loop." It is not. It is "add it without moving control into a loop the model owns." Those are different, and keeping them different is what lets a deterministic system grow bounded intelligence later without a rewrite.&lt;/p&gt;

&lt;p&gt;If you expect to add diagnosis, repair recommendation, or deeper failure analysis down the line, build the seams now, cheaply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replayable events.&lt;/strong&gt; Every transition and finding emitted as a durable, ordered record, so a later reasoning step can replay exactly what happened instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independently addressable stages.&lt;/strong&gt; Keep the pipeline decomposed, so a diagnosis step can point at one stage without unpicking the whole run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A read-only retrieval seam.&lt;/strong&gt; A way to pull prior records and context as &lt;em&gt;evidence&lt;/em&gt; for a diagnosis, without letting the diagnosis mutate the verdict path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-verdict hooks.&lt;/strong&gt; Failure analysis runs &lt;em&gt;after&lt;/em&gt; the reproducible verdict, never inside it, so it can never change the result it is analysing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advisory output only.&lt;/strong&gt; A repair step produces a suggested fix, a rationale, and a confidence. Whether to apply, retry, or escalate stays the orchestrator's call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The invariant across all of them is the law restated: repair and retry are controlled by the orchestrator, not by a model acting on its own. Anything that would move control into an autonomous loop is not a small extension, it is a change to the fundamental property of the system, and it should be treated as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same law, one level down
&lt;/h2&gt;

&lt;p&gt;These three pieces are one argument at three depths. The first said correctness comes from structure around the generator, not from a bigger generator. The second said the engineering is minimizing the fraction of output only the model owns. This one says: of the part you keep deterministic, choose the determinism that fits the question, a rule for facts, a state machine for flow, and of the part you keep probabilistic, let the model reason as much as the problem needs but never let it hold the wheel.&lt;/p&gt;

&lt;p&gt;The probabilistic part proposes. The deterministic part disposes. And control, the decision of what happens next and when the work is done, is deterministic, always. That is not a constraint on what your system can do. It is the thing that makes what it does trustworthy, reproducible, cheap, and yours to debug at three in the morning when it matters.&lt;/p&gt;

&lt;p&gt;Determinism where you can. Judgement where you must. Control never to the agent.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the third piece in the arc. The first: &lt;a href="https://harrisonsec.com/blog/generative-ai-builds-shapes-not-games/" rel="noopener noreferrer"&gt;Generative AI Builds Shapes, Not Games&lt;/a&gt;. The second: &lt;a href="https://harrisonsec.com/blog/shrink-the-stochastic-surface/" rel="noopener noreferrer"&gt;Shrink the Stochastic Surface&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Related, the same law at other layers: &lt;a href="https://harrisonsec.com/blog/agent-memory-is-a-cache-coherence-problem/" rel="noopener noreferrer"&gt;Agent Memory Is a Cache Coherence Problem&lt;/a&gt; and &lt;a href="https://harrisonsec.com/blog/agent-architecture-compute-allocation-advisor-strategy/" rel="noopener noreferrer"&gt;Agent Architecture Is a Compute Allocation Problem: The Advisor Strategy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>Shrink the Stochastic Surface: A Design Standard for Probabilistic Systems</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Tue, 30 Jun 2026 16:08:30 +0000</pubDate>
      <link>https://dev.to/harrisonsec/shrink-the-stochastic-surface-a-design-standard-for-probabilistic-systems-58n2</link>
      <guid>https://dev.to/harrisonsec/shrink-the-stochastic-surface-a-design-standard-for-probabilistic-systems-58n2</guid>
      <description>&lt;p&gt;I keep writing variations of the same sentence. Agent memory has to terminate at a source of truth. An agent loop has to terminate at a check. Generative 3D has to terminate at a verifier. The probabilistic part proposes, a deterministic part disposes.&lt;/p&gt;

&lt;p&gt;Four pieces, one shape. That is usually a sign there is a law underneath, not four coincidences. This is my attempt to write the law down, and then to turn it into a standard I can apply to the next system before it ships instead of after it breaks.&lt;/p&gt;

&lt;p&gt;The companion piece argued that generative AI builds plausible shapes and that correctness has to come from structure and verification wrapped around the generator. That answers &lt;em&gt;whether&lt;/em&gt; you need something outside the model. It does not answer &lt;em&gt;how much&lt;/em&gt;, or &lt;em&gt;where the line goes&lt;/em&gt;. This piece is about the line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quantity nobody names
&lt;/h2&gt;

&lt;p&gt;Every probabilistic system has a number attached to it that almost no one states out loud: the fraction of its output whose correctness rests on a model getting it right, with nothing deterministic to anchor or check that part.&lt;/p&gt;

&lt;p&gt;Call it the &lt;strong&gt;stochastic surface&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A pure end-to-end model has a stochastic surface of one. Everything it emits is a sample, and every sample is trusted as final. A pocket calculator has a stochastic surface of zero. A real system sits between, and the exact position is the single most important design decision in the thing, more than the model, more than the prompt, more than the framework.&lt;/p&gt;

&lt;p&gt;Here is the claim, stated as plainly as I can:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The reliability of a probabilistic system is bounded by the size of its stochastic surface. Reliability engineering is surface reduction: pushing as much of the output as possible onto deterministic structure, and leaving the model only the irreducible part that genuinely has no anchor.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not a style preference. It follows from what a sample is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the surface bounds reliability
&lt;/h2&gt;

&lt;p&gt;A deterministic component has a property a sampler never has: when it is wrong, it is wrong the same way every time, and you can find out. A unit test fails. A constraint is violated. A schema rejects the payload. The error is &lt;em&gt;locatable&lt;/em&gt;, which means it is &lt;em&gt;fixable&lt;/em&gt;, which means over time that part of the system trends toward correct and stays there.&lt;/p&gt;

&lt;p&gt;A sample has none of this. It is drawn fresh, it can be wrong differently every time, and, crucially, &lt;strong&gt;it carries no signal about whether it is wrong&lt;/strong&gt;. A plausible-looking output and a correct output are indistinguishable from inside the generator. That is the entire problem in one sentence: the model cannot tell you it succeeded, so it cannot be trusted even when it did.&lt;/p&gt;

&lt;p&gt;Now stack many such samples. If thirty percent of your output is on the stochastic surface, then thirty percent of every result is an unverified claim that could be confidently, undetectably wrong, and you have no map of which thirty percent. The failures do not announce themselves. They surface downstream, far from where they were born, as a doorless cottage the workflow was perfectly happy to call done.&lt;/p&gt;

&lt;p&gt;So the rough mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reliability ≈ 1 − (unanchored stochastic fraction)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not literally a formula you compute, but the right intuition. Every percent of output you move off the surface, by retrieving it instead of inventing it, by deriving it from a rule instead of sampling it, by checking it against a spec, is a percent that stops being an undetectable liability. You do not make a probabilistic system reliable by making the model better. You make it reliable by giving the model less of the output to be solely responsible for.&lt;/p&gt;

&lt;p&gt;That reframes everything. It means the work is not "pick the best model." It is "draw the smallest possible circle around the part only a model can do, and build deterministic structure everywhere else." Two sides of a system show this most clearly: how you generate, and how you evaluate. They turn out to be the same move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generation side: anchor, don't invent
&lt;/h2&gt;

&lt;p&gt;When you need a model to produce something, the lazy default is to describe it and let the model conjure it from the prior. That maximizes the stochastic surface on purpose. Almost always you can do better, and the options form a clean ladder ordered by &lt;strong&gt;how many bits the model has to invent&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 1: retrieve, then edit
&lt;/h3&gt;

&lt;p&gt;The cheapest and most underrated move: do not generate from scratch, find the nearest high-quality real example and modify only what the task requires.&lt;/p&gt;

&lt;p&gt;In a game studio this is the difference between "generate a medieval cottage" and "here is a hand-built cottage our artists shipped, adapt it to a 15 by 15 footprint with a south door." The first asks the model to hallucinate an entire artifact. The second hands it a correct starting point and asks for a delta. The stochastic surface collapses from the whole object to the edit.&lt;/p&gt;

&lt;p&gt;The principle generalizes far past games. Retrieval-augmented generation is this move for text. Asking a coding model to modify an existing, tested function instead of writing one blind is this move for code. In every case the logic is identical: &lt;strong&gt;the model's real workload is total information minus retrievable information.&lt;/strong&gt; Anything already present in a real example is information the model does not have to invent, and therefore cannot get wrong. You spend a similarity search to buy down the surface. It is almost always a good trade.&lt;/p&gt;

&lt;p&gt;The failure mode to respect: retrieval anchors you to the retrieved thing, so retrieval quality becomes the new floor. Garbage neighbors give garbage edits. But notice what changed, the failure moved from an invisible generative hallucination to a &lt;em&gt;visible, checkable&lt;/em&gt; retrieval step you can inspect, score, and improve. That is surface reduction even when it is imperfect, because it converts an unanchored failure into an anchored one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 2: fine-tune to narrow the distribution
&lt;/h3&gt;

&lt;p&gt;Retrieval changes the &lt;em&gt;starting point&lt;/em&gt;. Fine-tuning changes the &lt;em&gt;distribution itself&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is the tier worth dwelling on, because it is the one most often reached for first and understood least. A base model samples from an enormous manifold of plausible-for-the-internet outputs. Fine-tuning on your own high-quality, domain-specific data reshapes that manifold so the model's default sample lands much closer to what your domain considers good. You are not teaching it new facts so much as &lt;strong&gt;moving the center of mass of its prior&lt;/strong&gt; onto your distribution, and shrinking the variance.&lt;/p&gt;

&lt;p&gt;For a studio, training a model on your own corpus of shipped, art-directed, style-consistent assets does something retrieval cannot: it makes the &lt;em&gt;typical&lt;/em&gt; generation on-style and on-spec, not just the retrieved-and-edited one. It raises the floor everywhere, including the cases where you have no neighbor to retrieve. The economics also favor it once volume is high enough: a one-time training cost amortized across millions of generations, versus paying for a large frontier prompt every single time.&lt;/p&gt;

&lt;p&gt;But be precise about what fine-tuning does and does not do to the stochastic surface, because this is where people over-trust it. Fine-tuning &lt;strong&gt;lowers the surface, it does not remove it.&lt;/strong&gt; A model fine-tuned on perfect 15 by 15 cottages will produce cottages that are &lt;em&gt;usually closer&lt;/em&gt; to 15 by 15. It still has no representation of "footprint equals 15 by 15" as a predicate to satisfy and check. It samples from a tighter distribution, but it is still sampling. The discrete constraint is still discrete, and a narrower continuous distribution is still continuous. Fine-tuning buys you a better-behaved sampler. It does not buy you a solver or a verifier, and treating a fine-tuned model as if it were one is exactly the over-trust this whole standard is built to prevent.&lt;/p&gt;

&lt;p&gt;The right read: fine-tuning is the strongest &lt;em&gt;generation-side&lt;/em&gt; lever, and generation-side levers have a ceiling. They make the proposal better. They never make the proposal self-disposing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 3: generate the residual only
&lt;/h3&gt;

&lt;p&gt;After you have retrieved what you can and narrowed what you can, whatever is left, the genuinely novel part with no prior to anchor to, is what you let the model invent freely. That residual is where a generative prior earns its keep: plausible, varied, rich single forms. The sphere and the gatehouse from the companion piece are this tier done well.&lt;/p&gt;

&lt;p&gt;The discipline is to make that residual as small as the task allows, and to wrap it, never to let it stand as the final word. Which is the other side of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evaluation side: rules first, model last
&lt;/h2&gt;

&lt;p&gt;Now flip from producing output to judging it. The same law applies, and the anti-pattern is more seductive because it looks like progress: hand the whole output to an LLM and ask "is this good?"&lt;/p&gt;

&lt;p&gt;That puts your evaluation's stochastic surface at one hundred percent. You have built a judge that cannot tell you when it is wrong, to assess a generator that cannot tell you when &lt;em&gt;it&lt;/em&gt; is wrong. Two unanchored samplers in a trench coat. It demos beautifully and rots quietly.&lt;/p&gt;

&lt;p&gt;The reliable structure is a hierarchy, ordered by how anchored each layer is, and you push as much weight as possible to the top:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quantifiable, to a deterministic metric.&lt;/strong&gt; Anything you can measure, you measure. Footprint dimensions. Block counts. Latency. Compile success. Test pass rate. Schema validity. Forbidden-element count equal to zero. This is the bedrock layer and it should carry the most weight the domain allows, because it has a stochastic surface of zero. It is right the same way every time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Formalizable but not numeric, to a program check.&lt;/strong&gt; Things that are not a number but are still decidable: is the door actually passable (run a pathfinder), does the graph have the required structure, does the config satisfy this invariant. Still deterministic, still locatable, still zero surface. This is the procedural and symbolic layer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Irreducibly subjective, to a bounded model judge.&lt;/strong&gt; What is genuinely left, "is this &lt;em&gt;fun&lt;/em&gt;," "is this on-brand," "is this elegant", goes to an LLM. But scoped: a small, well-defined slice, with a rubric, calibrated against human ratings, and auditable after the fact. The model judges the residual, not the whole.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The number that matters is the &lt;strong&gt;share of your evaluation weight sitting in layers 1 and 2.&lt;/strong&gt; That is your evaluation's anchored fraction, and it is the inverse of its stochastic surface. A good eval system is one where the subjective model-judged slice is small and shrinking.&lt;/p&gt;

&lt;p&gt;And here is the part that makes evaluation more than a one-time setup. Every time you take a judgment that used to live in the model's head, "this looks too wide," and turn it into a rule, "footprint must equal the spec," you move it from layer 3 to layer 1 &lt;strong&gt;permanently&lt;/strong&gt;. It never goes back. Evaluation built this way is a &lt;em&gt;ratchet&lt;/em&gt;: a mechanism for steadily converting accumulated human judgment into deterministic checks, one notch at a time, each notch shrinking the surface and never releasing it. "Let the AI grade the AI" is the opposite, a surface stuck at one hundred percent with no mechanism to ever bring it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two sides are one move
&lt;/h2&gt;

&lt;p&gt;Step back and the generation ladder and the evaluation hierarchy are the same diagram viewed from two ends.&lt;/p&gt;

&lt;p&gt;On both sides you are drawing a line. On one side of the line is the deterministic part: retrieved examples, narrowed distributions, constraint solvers, metrics, program checks. On the other side is the irreducible stochastic residual: the novel shape, the genuinely subjective call. &lt;strong&gt;That line is the stochastic surface.&lt;/strong&gt; Generation and evaluation are just its two projections, one for producing, one for judging.&lt;/p&gt;

&lt;p&gt;Every system I keep writing about is an instance of drawing that line well. Agent memory fails when a model's hedge gets stored as a fact with no source to check it against; the fix anchors memory to a source of truth, shrinking the surface. The advisor strategy spends a cheap model on the bulk and reserves expensive, decisive compute for the few points that must be right; that is surface reduction in the cost dimension. Retrieval versus grep, plan-generate-solve-verify for game content, rules-first evaluation, all the same law: &lt;strong&gt;the probabilistic component proposes, and a deterministic component, as much of one as the problem allows, disposes.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The standard
&lt;/h2&gt;

&lt;p&gt;The point of a law is to use it before you ship, not to explain the wreckage after. So here is the checklist I am going to run on the next probabilistic system I build, and the one after that. Five questions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What is the stochastic surface?&lt;/strong&gt; Name the exact fraction of the output whose correctness rests on the model alone, unretrieved and unchecked. If you cannot point to it, you do not understand the system yet. Stop here until you can.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What did you anchor instead of invent?&lt;/strong&gt; For every generated part, did you retrieve a real example first (tier 1), narrow the distribution with fine-tuning where volume justifies it (tier 2), and reserve free generation for the residual only (tier 3)? Each part that skipped the ladder is surface you chose not to reduce, and you should be able to say why.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What is checkable, and is it checked?&lt;/strong&gt; Every predicate the output must satisfy that &lt;em&gt;can&lt;/em&gt; be verified deterministically, is it? Sizes, counts, schemas, invariants, passability. Anything checkable but unchecked is the worst category: a failure you could have caught for free and did not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Where does the model judge, and is that slice bounded?&lt;/strong&gt; In evaluation, what share of weight is deterministic (layers 1 and 2) versus model-judged (layer 3)? Is the judged slice small, rubric-bound, calibrated, and auditable? A universal LLM judge is a red flag, not a feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is the surface shrinking over time?&lt;/strong&gt; Is there a ratchet, a path by which recurring human judgments become permanent rules and the surface trends down with use? A system whose surface is fixed will not get more reliable no matter how long it runs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Score every component against these five and the weak points light up immediately. They are always the same shape: a part handed to the model that could have been retrieved, derived, or checked, and was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the standard stops
&lt;/h2&gt;

&lt;p&gt;A law worth trusting comes with its boundary, so here is mine. &lt;strong&gt;This standard governs correctness tasks, and only those.&lt;/strong&gt; A correctness task has a spec the output must satisfy: a size, a passable door, a compiling program, a routed circuit, a factual answer. There, a large stochastic surface is pure liability and shrinking it is the whole job.&lt;/p&gt;

&lt;p&gt;But some tasks have no spec, and for those the standard inverts. Style exploration. Brainstorming. A first rough draft meant to be thrown away. Concept art whose only requirement is "show me something I would not have thought of." There the stochastic surface &lt;em&gt;should&lt;/em&gt; be near one hundred percent, because there is no predicate to anchor to and constraints would only strangle the thing you wanted. Clamping a creative task with a verifier is the same category error as trusting a generator on a correctness task, run backwards.&lt;/p&gt;

&lt;p&gt;So the discipline underneath the discipline is telling the two apart. Ask of any output: is there a spec it must satisfy, or am I sampling for novelty? If there is a spec, the standard applies and you shrink the surface as far as the problem allows. If there is not, let the model run, and do not pretend a verifier was missing.&lt;/p&gt;

&lt;p&gt;The hard engineering was never the model. An adequate one is already here, and the next one will be better in ways that do not touch this. The hard engineering is drawing the line around it, and making that line as small as the problem allows.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the second piece in a pair. The first: &lt;a href="https://harrisonsec.com/blog/generative-ai-builds-shapes-not-games/" rel="noopener noreferrer"&gt;Generative AI Builds Shapes, Not Games&lt;/a&gt;.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Related, the same law at other layers: &lt;a href="https://harrisonsec.com/blog/agent-memory-is-a-cache-coherence-problem/" rel="noopener noreferrer"&gt;Agent Memory Is a Cache Coherence Problem&lt;/a&gt; and &lt;a href="https://harrisonsec.com/blog/agent-architecture-compute-allocation-advisor-strategy/" rel="noopener noreferrer"&gt;Agent Architecture Is a Compute Allocation Problem: The Advisor Strategy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>Generative AI Builds Shapes, Not Games: The Constraint Gap and the Architecture That Closes It</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:40:52 +0000</pubDate>
      <link>https://dev.to/harrisonsec/generative-ai-builds-shapes-not-games-the-constraint-gap-and-the-architecture-that-closes-it-2e30</link>
      <guid>https://dev.to/harrisonsec/generative-ai-builds-shapes-not-games-the-constraint-gap-and-the-architecture-that-closes-it-2e30</guid>
      <description>&lt;p&gt;I sat down to benchmark a tool and ended up with a map of a wall.&lt;/p&gt;

&lt;p&gt;Higgsfield shipped a Minecraft "prompt-to-build" feature: type a prompt, get a structure in-world about a minute later. I ran eight building prompts through it, scored each one, and walked through the results. The point started as "how good is this tool." It ended somewhere more useful, because the &lt;em&gt;shape&lt;/em&gt; of the failures turned out to be a clean read on where generative AI hits a wall in game content, and why, and what the architecture that gets past it has to look like.&lt;/p&gt;

&lt;p&gt;The one-sentence version: generative models are &lt;strong&gt;plausibility engines&lt;/strong&gt;, and games need &lt;strong&gt;correctness engines&lt;/strong&gt;. Those are not the same machine, and you do not get one by scaling the other.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — An AI Minecraft builder produced recognizable single forms (a sphere, a tower, a castle gatehouse with a genuinely walkable gate) in about a minute, but dropped exact sizes, named materials, and door positions, failed to compose all three multi-object scenes I gave it, satisfied a "no lava" rule only vacuously (it places no fluids at all), and surfaced no signal for whether any output met the prompt. That pattern is the signature of what generative models are: samplers over a distribution of &lt;em&gt;forms&lt;/em&gt;. Game content demands three things a form-sampler structurally lacks: discrete constraint satisfaction, compositional structure, and functional correctness with verification. Scaling adds plausible shapes, not those three capabilities. The architecture that closes the gap separates the continuous from the discrete: a symbolic planner emits a scene graph and explicit constraints, generative models fill per-object shape, a solver places objects to satisfy the constraints, and a verifier checks the result and repairs failures. Plausibility from the generator; correctness from structure and verification.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The evidence, briefly
&lt;/h2&gt;

&lt;p&gt;The full hands-on benchmark, per-prompt scores, and figures are in the &lt;a href="https://dev.to/blog/i-tested-higgsfield-minecraft-prompt-to-build/"&gt;companion findings post&lt;/a&gt;. The compressed version is enough to ground the argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What worked.&lt;/strong&gt; Single cohesive forms with a strong visual prior came out fast and recognizable. "A giant sphere" produced a clean voxel sphere. A watchtower read as a tower. A gatehouse with "two towers and a central gate players can walk through" came back as exactly that, in about a minute, and the gate was genuinely passable when I walked through it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxn10qgjf5h022ifiykns.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxn10qgjf5h022ifiykns.jpg" alt="Gatehouse: two towers, a central arch, a passable gate" width="800" height="517"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The strongest result. A castle gatehouse is one cohesive, canonical form with named sub-parts, and it generated fast and well.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What didn't.&lt;/strong&gt; The moment a prompt depended on discrete, checkable requirements, those requirements fell away. "A 15 by 15 block cottage using mostly wood and stone, entrance on the south side, inside walkable" came back as a doorless lumpy grey wall, far wider than 15×15, no wood, no way in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqipq98wzvoexu3lep0iy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqipq98wzvoexu3lep0iy.jpg" alt="The 15x15 cottage prompt: a doorless lumpy wall" width="800" height="517"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The most constrained prompt produced the worst result. Every discrete requirement, size, material, door, enclosure, failed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Multi-object scenes failed three different ways across three prompts: one hung with no output, one scattered into wildly inconsistent scale, one collapsed two tents and a campfire into a single teal mound. And the negative constraint ("do not use glass, lava, water, or redstone") was "honored" only because the builder never places fluids at all. A lava-colored band read in-game as solid &lt;code&gt;orange_concrete&lt;/code&gt; with no fluid present. The rule was satisfied vacuously, by palette limitation, not by following it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwapelgyv0kvtwllagh4u.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwapelgyv0kvtwllagh4u.jpg" alt="F3 block check: the lava-colored band is solid orange_concrete, no fluid" width="800" height="517"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The negative constraint met vacuously: a color-matched solid block, not a parsed-and-honored "do not use."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two structural facts sit underneath all of it. The behavior is consistent with a &lt;strong&gt;mesh-generation plus voxelization pipeline&lt;/strong&gt;: produce one 3D mesh, voxelize it, color-map to a block palette, place it. (I did not decompile or trace it, so treat that as the most likely explanation, not confirmed.) And there was &lt;strong&gt;no validation signal&lt;/strong&gt; anywhere: nothing in the workflow indicated whether the output satisfied size, material, door, or function.&lt;/p&gt;

&lt;p&gt;Hold those two facts. They are the whole argument in miniature: one form, no structure, no check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens: plausibility is not correctness
&lt;/h2&gt;

&lt;p&gt;It is tempting to read the cottage as a bug, a model that needs more training. That misreads what the model is.&lt;/p&gt;

&lt;p&gt;A generative 3D model is a &lt;strong&gt;sampler over a learned distribution of forms&lt;/strong&gt;. Training teaches it the manifold of plausible shapes for a text condition; inference draws a point from it. This is the right machine for one job, "give me a plausible instance of X," and it is genuinely good at it. The sphere and the gatehouse are that job done well.&lt;/p&gt;

&lt;p&gt;Game content asks for a different job: not "a plausible instance" but "an instance that &lt;em&gt;satisfies these requirements&lt;/em&gt;." And the requirements games impose come in three flavors a form-sampler has no mechanism for.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Discrete constraints
&lt;/h3&gt;

&lt;p&gt;"15×15." "South-facing door." "No lava." These are discrete, symbolic predicates. They are either satisfied or not, and you can check which.&lt;/p&gt;

&lt;p&gt;A continuous sampler has no place to &lt;em&gt;put&lt;/em&gt; a discrete predicate. It can make outputs that are distributionally &lt;em&gt;consistent with the words&lt;/em&gt; "15 by 15", things that tend to be smallish and square-ish, but it cannot &lt;em&gt;satisfy the predicate&lt;/em&gt; "footprint equals 15×15" because nothing in the architecture represents that predicate as a thing to be satisfied and checked. This is the same root cause behind image models that botch exact finger counts and legible text: counts and letters are discrete, and a plausibility sampler approximates them instead of satisfying them. The cottage's missing door is not a training gap. It is a category error to expect a form-sampler to honor a positional predicate.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Compositional structure
&lt;/h3&gt;

&lt;p&gt;A single object is a form. A &lt;em&gt;scene&lt;/em&gt; is a graph: objects as nodes, spatial relations as edges. "Three houses along a path with a tree between each" is a structured arrangement, not a shape.&lt;/p&gt;

&lt;p&gt;A monolithic mesh generator has no node-and-edge representation to build that graph in. Asked for a scene, it has only one move available: hallucinate the entire arrangement as a single form and voxelize it. The three scene failures are three ways that move degrades, hang, scatter, collapse, but they share one cause: there is no scene graph, so there is no composition, only a blob that gestures at the elements. "It cannot do scenes" is too strong; "it has no structured representation in which a scene could be composed" is exact.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Functional correctness, and the missing verifier
&lt;/h3&gt;

&lt;p&gt;"Players can walk through the gate" is a &lt;em&gt;functional&lt;/em&gt; property. You cannot read it off the geometry by eye with confidence; you confirm it by testing, by trying to walk the path. When the gatehouse gate turned out passable, that was the shape prior paying off, not the system knowing or checking that the function held. There is no notion of function in a form-sampler, and, more tellingly, no loop that asks "did the output satisfy the ask?" after generating.&lt;/p&gt;

&lt;p&gt;That missing loop is the deepest part. Even a model that frequently lands constraints by luck is unreliable without a verifier, because nothing distinguishes the lucky output from the failed one. The workflow had no score, no self-check, no "this build is 14×16, not 15×15." Generation without verification cannot tell you it succeeded, which means it cannot be trusted even when it did.&lt;/p&gt;

&lt;h3&gt;
  
  
  The unifying diagnosis
&lt;/h3&gt;

&lt;p&gt;Stack the three together and the diagnosis is one line: &lt;strong&gt;generative models optimize plausibility; game content requires correctness, and correctness is discrete, compositional, and functional.&lt;/strong&gt; Plausibility lives on a continuous manifold. Correctness is symbolic, structured, and checkable. They are different mathematical objects, and one architecture is built for the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why scaling alone won't close it
&lt;/h2&gt;

&lt;p&gt;The reflex in 2026 is "the next model will fix this." For this gap, scaling the same architecture has little reason to close it and some reason not to. A different architecture might; a bigger form-sampler won't.&lt;/p&gt;

&lt;p&gt;More parameters and more data make the sampler draw &lt;em&gt;more plausible shapes, more faithfully&lt;/em&gt; from the form distribution. That is real progress on the axis the architecture already optimizes. It does not add a discrete constraint representation, because the training objective never asks the model to satisfy and check a predicate. It does not add a scene graph, because the output is still one mesh. It does not add a verifier, because verification is a separate computation the generator was never built to perform.&lt;/p&gt;

&lt;p&gt;You can see the shape of this in the parts of the benchmark that &lt;em&gt;did&lt;/em&gt; improve with the form prior, versus the parts that did not. The gatehouse, more canonical, came out better than the watchtower. Scaling pushes everything along &lt;em&gt;that&lt;/em&gt; axis: better, more canonical forms. The cottage's door does not live on that axis at all. No amount of "better sphere" becomes "satisfies 15×15 with a south door," any more than a sharper camera becomes a tape measure.&lt;/p&gt;

&lt;p&gt;The gap is architectural. Closing it means adding the missing architecture, not enlarging the existing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is bigger than Minecraft
&lt;/h2&gt;

&lt;p&gt;The Minecraft builder is a clean, cheap microcosm because Minecraft makes correctness &lt;em&gt;legible&lt;/em&gt;, you can literally F3 a block and read whether the constraint held. But the same wall stands wherever an output has to &lt;em&gt;satisfy a spec&lt;/em&gt; rather than merely &lt;em&gt;look right&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CAD and mechanical design:&lt;/strong&gt; a part that looks like a bracket but is 2mm off the bolt pattern is scrap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture and floor plans:&lt;/strong&gt; a plausible-looking plan with a bedroom you can't reach is not a plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit and chip layout:&lt;/strong&gt; plausible is meaningless; it routes and meets timing, or it doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code generation:&lt;/strong&gt; "looks like correct code" is exactly the trap; it compiles and passes tests, or it doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level and quest design:&lt;/strong&gt; a level must be completable, not just atmospheric.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is the same split: &lt;em&gt;plausible&lt;/em&gt; (continuous, distributional, what the generator gives you) versus &lt;em&gt;correct&lt;/em&gt; (discrete, structured, functional, what the domain demands). Game building is a vivid instance because it bundles all three correctness flavors, dimensional, compositional, functional, into one minute-long generation you can inspect. The lesson generalizes to all of production-grade generative content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture that closes the gap
&lt;/h2&gt;

&lt;p&gt;If one model can't be plausibility engine and correctness engine at once, stop asking it to be. Split the pipeline so that the continuous and the discrete each go to the machine built for them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Plan — symbolic.&lt;/strong&gt; A planner (an LLM or a program synthesizer) turns the prompt into a &lt;em&gt;structured spec&lt;/em&gt;: a scene graph (objects and spatial relations) plus explicit constraints (sizes, materials, positions, forbidden sets, functional requirements like "this gate is a passable path"). This is the discrete representation the form-sampler lacks. "15×15, south door, wood and stone" becomes machine-checkable slots, not vibes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Generate — continuous.&lt;/strong&gt; Per-object generative models produce the &lt;em&gt;shapes&lt;/em&gt;, conditioned on the spec's slots. This is exactly where the generative prior earns its keep: plausible, varied, rich single forms. The sphere and the gatehouse show the generator is already good at this when it is asked only for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Place and solve — symbolic.&lt;/strong&gt; A constraint solver or procedural placement layer arranges the generated objects to satisfy the spatial, dimensional, and adjacency constraints. This is not new technology; it is the &lt;strong&gt;procedural-generation toolbox games have used for thirty years&lt;/strong&gt;, wave-function-collapse, shape grammars, constraint-based layout, now used to &lt;em&gt;arrange generative outputs&lt;/em&gt; instead of hand-authored tiles. Determinism and satisfiability are features here, not limitations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Verify and repair — the loop the benchmark had none of.&lt;/strong&gt; A checker evaluates the &lt;em&gt;checkable&lt;/em&gt; predicates against the spec: is the footprint 15×15? is the door passable? are there zero forbidden blocks? does the object count match? Failures route back to regeneration or local repair. This is the validation signal whose absence was the most telling thing in the whole session.&lt;/p&gt;

&lt;p&gt;Plausibility comes from step 2. Correctness comes from steps 1, 3, and 4. The generator stops being asked to do the job it can't and gets to do the job it's good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deeper pattern: trust terminates outside the generator
&lt;/h2&gt;

&lt;p&gt;This is the part that ties back to everything I've been writing about agents this year, and it's why the Minecraft toy matters more than a toy should.&lt;/p&gt;

&lt;p&gt;The recurring failure of probabilistic systems is treating the generator's output as ground truth. In &lt;a href="https://dev.to/blog/agent-memory-is-a-cache-coherence-problem/"&gt;agent memory&lt;/a&gt;, the failure mode is a model's "the user &lt;em&gt;could&lt;/em&gt; do X" getting stored and later read back as "the user &lt;em&gt;did&lt;/em&gt; X", a probabilistic hedge flattened into a fact, with no source to check it against. In the &lt;a href="https://dev.to/blog/agent-architecture-compute-allocation-advisor-strategy/"&gt;advisor strategy&lt;/a&gt;, the design that works puts a cheap model on the bulk of the work and reserves the expensive, decisive computation for the few points that actually need to be right.&lt;/p&gt;

&lt;p&gt;Generative 3D for games is the same principle in a different costume. The generator is the bulk path: cheap, fast, plausible, and &lt;em&gt;not to be trusted on its own&lt;/em&gt; for any property that has to be correct. Correctness has to terminate at something outside the generator, a spec, a solver, a verifier, the same way agent memory has to terminate at a source of truth and an agent loop has to terminate at a check. &lt;strong&gt;The probabilistic component proposes; a deterministic component disposes.&lt;/strong&gt; Systems that wire it that way are reliable. Systems that trust the sampler's output as final are plausible right up until they are confidently, unverifiably wrong, a doorless cottage that the workflow was perfectly happy to call done.&lt;/p&gt;

&lt;p&gt;The winners in AI content generation will not have the best single-shape model. An adequate generator is already here. They will have the best &lt;em&gt;structure and verification wrapped around&lt;/em&gt; the generator: the planner that emits a checkable spec, the solver that satisfies it, the verifier that proves it. That is where the engineering is, and it is the half the current monolithic tools skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outlook
&lt;/h2&gt;

&lt;p&gt;Three things follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monolithic text-to-3D is a phase, not a destination.&lt;/strong&gt; The single-prompt-to-single-mesh tool is the generative-AI equivalent of an early language model asked to do arithmetic in its head. The field moves toward decomposed, plan-generate-verify pipelines for the same reason agents moved toward tool use and verification: the monolith is plausible and the pipeline is correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Procedural generation gets a second life, not a retirement.&lt;/strong&gt; PCG was always deterministic and constraint-satisfying and always limited in variety and richness. Generative models are the inverse. The synthesis, generative priors for per-object shape inside a procedural and constraint-solving frame, gives you both, and the verification loop makes it trustworthy. The 30-year toolbox is the missing half of generative AI for games, not its casualty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The benchmark is a snapshot of the monolithic era.&lt;/strong&gt; Re-run the same eight prompts against a plan-generate-solve-verify system and the cottage is 15×15 with a south door, because a solver placed it and a verifier checked it, not because a bigger model finally drew it. That is the test I'd want to see, and the architecture I'd bet on.&lt;/p&gt;

&lt;p&gt;Generative AI can build a shape. Building a game asks for correctness, and correctness is a different machine. The interesting work, in games and well beyond them, is in building the second machine around the first.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Empirical companion (the hands-on benchmark, full scores and figures): &lt;a href="https://dev.to/blog/i-tested-higgsfield-minecraft-prompt-to-build/"&gt;I Tested Higgsfield's Minecraft "Prompt-to-Build"&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Related: &lt;a href="https://dev.to/blog/agent-memory-is-a-cache-coherence-problem/"&gt;Agent Memory Is a Cache Coherence Problem&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Related: &lt;a href="https://dev.to/blog/agent-architecture-compute-allocation-advisor-strategy/"&gt;Agent Architecture Is a Compute Allocation Problem: The Advisor Strategy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>gamedev</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Tested Higgsfield's Minecraft Prompt-to-Build. It Generates Shapes, Not Scenes.</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Thu, 18 Jun 2026 20:03:42 +0000</pubDate>
      <link>https://dev.to/harrisonsec/i-tested-higgsfields-minecraft-prompt-to-build-it-generates-shapes-not-scenes-2b72</link>
      <guid>https://dev.to/harrisonsec/i-tested-higgsfields-minecraft-prompt-to-build-it-generates-shapes-not-scenes-2b72</guid>
      <description>&lt;p&gt;Higgsfield shipped a Minecraft "prompt-to-build" feature: a mod that drops a "Supercomputer" block into your world, takes a free-text prompt, and generates a structure in-world a minute later. I spent one session putting real building prompts through it to see what it actually does, not what the landing page says it does. Eight prompts, fixed screenshots, an in-world walkthrough, and a scoring rubric.&lt;/p&gt;

&lt;p&gt;The short version: it behaves like a &lt;strong&gt;single-cohesive-3D-form generator with strong canonical priors&lt;/strong&gt;, not an architecture or scene engine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — Higgsfield's in-world prompt-to-build produced recognizable single forms (a sphere, a tower, a castle gatehouse, including a functional walkable gate) in about a minute. But in my samples it dropped discrete constraints (exact size, specified materials, door position), failed to compose a coherent multi-object scene in all three scene prompts I tried, and exposed no validation signal for whether the output met the prompt. The behavior is consistent with a mesh-to-voxel pipeline: generate one shape, color-map it to blocks. Strong on shape, weak on constraints, composition, and function.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How it appears to work (inferred from behavior)
&lt;/h2&gt;

&lt;p&gt;I did not decompile the mod or capture a network trace, so this is the most likely explanation, not a confirmed fact. The observed behavior is &lt;strong&gt;consistent with a mesh-generation plus voxelization pipeline&lt;/strong&gt;: a text prompt produces a 3D mesh in the cloud, which is then voxelized, mapped to a limited block palette, and placed in-world.&lt;/p&gt;

&lt;p&gt;If that model is right, it accounts for most of what I saw. A color or texture sampler would pick block &lt;em&gt;colors&lt;/em&gt; rather than &lt;em&gt;materials&lt;/em&gt;. A mesh encodes a &lt;em&gt;shape&lt;/em&gt;, not discrete numeric or positional constraints. And a layout of separate objects has no single form to generate from. One in-game block check supports it directly: a region that looked like lava read as &lt;code&gt;minecraft:orange_concrete&lt;/code&gt; with no fluid placed at all — a solid block chosen by color.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Environment:&lt;/strong&gt; Minecraft Java 1.21.1 + NeoForge 21.1.233 + the Higgsfield mod, creative mode, superflat world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flow:&lt;/strong&gt; place the Higgsfield "Supercomputer" block, set &lt;code&gt;Type: Structure&lt;/code&gt;, enter the prompt, insert a blank Structure medium, Generate, then print the result in-world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; about 1.15 credits per build (the UI estimates 2). Failed jobs are not charged — the one prompt that timed out cost nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts:&lt;/strong&gt; eight total — a single object, a constrained object, a functional compound form, a negative-constraint prompt, and three multi-object scene prompts — plus one geometric-primitive control (a sphere) to check that outputs are genuinely prompt-conditioned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring:&lt;/strong&gt; 1 to 5 per dimension (prompt adherence, constraint adherence, spatial/functional, editability, visual, reliability). Single rater, from fixed screenshots plus an in-world walkthrough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yd30uijlbszp9xbndfs.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3yd30uijlbszp9xbndfs.jpg" alt="The Higgsfield Supercomputer panel in Minecraft" width="800" height="496"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The in-world "Supercomputer" panel: a free-text prompt, &lt;code&gt;Type: Structure&lt;/code&gt;, a blank Structure medium, and a credit estimate. This is the entire interface.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Prompt class&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Adher&lt;/th&gt;
&lt;th&gt;Constr&lt;/th&gt;
&lt;th&gt;Spatial&lt;/th&gt;
&lt;th&gt;Edit&lt;/th&gt;
&lt;th&gt;Visual&lt;/th&gt;
&lt;th&gt;Reliab&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;single object — watchtower&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;td&gt;recognizable tower, wrong material/size&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;object + discrete constraints — 15×15 cottage&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;td&gt;doorless lumpy wall, unrecognizable&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ctrl&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;geometric primitive — "a giant sphere"&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;td&gt;clean voxel sphere&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;multi-object scene — market&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8+ min&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;timeout, no output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;functional compound form — gatehouse&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;strong:&lt;/strong&gt; 2 towers + walkable gate&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;multi-object scene — houses + path + trees&lt;/td&gt;
&lt;td&gt;~2 min&lt;/td&gt;
&lt;td&gt;elements present, incoherent scale/layout&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;multi-object scene — campsite&lt;/td&gt;
&lt;td&gt;~2.5 min&lt;/td&gt;
&lt;td&gt;collapsed into one teal blob&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;negative constraint — 8×8 base, no glass/lava/water/redstone&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;td&gt;giant slab, not 8×8&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Per-prompt findings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  P1 — single object: watchtower
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Build a small wooden watchtower, 10 blocks tall, with a ladder, a roof, and a viewing platform."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6sdn2iz9us5afzppq1c6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6sdn2iz9us5afzppq1c6.jpg" alt="Watchtower result" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It reads clearly as a tall tower, so the &lt;em&gt;shape&lt;/em&gt; prior comes through. The discrete constraints did not. "Wooden" came out as orange terracotta and honeycomb blocks — the output matched colors, not the material word "wood." "10 blocks tall" became 20-plus. The ladder, roof, and platform are vaguely suggested by geometry but are not functional Minecraft elements. The surface shows the voxelization artifacts you'd expect: eroded edges, speckled palette quantization, asymmetry.&lt;/p&gt;

&lt;h3&gt;
  
  
  P2 — object + discrete constraints: 15×15 cottage
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Build a 15 by 15 block cottage using mostly wood and stone. Entrance on the south side, inside walkable."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqipq98wzvoexu3lep0iy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqipq98wzvoexu3lep0iy.jpg" alt="Cottage result" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most constrained prompt produced the worst result. It does not read as a cottage at all — a long lumpy grey wall with a red-orange top band, random holes, and stray noise blocks. Every discrete constraint failed: the footprint is far wider than 15×15, it's a wall rather than an enclosed cottage, there's no wood, and there's no door on any side (confirmed in world — you cannot enter it). This is the clearest example of the pattern in my samples: the more a prompt depends on discrete, checkable requirements, the less of it came through.&lt;/p&gt;

&lt;h3&gt;
  
  
  Control — geometric primitive: "a giant sphere"
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"A giant sphere."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F14vqtws54zds6i5vjm9x.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F14vqtws54zds6i5vjm9x.jpg" alt="Sphere control" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I ran this to settle one question: are outputs actually driven by the prompt, or just canned blobs? The result is an unmistakable, clean voxel sphere with the classic concentric-ring stepping. That's strong evidence of prompt-conditioning, and with a fresh superflat world it rules out "it was already there." It also fits the pattern from the other direction: a clean geometric form came out clean. Output looked best when the prompt carried &lt;em&gt;no&lt;/em&gt; discrete semantic constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  P3 — multi-object scene: market (timeout)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Build a small village market: four stalls around a central well, with paths connecting them."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No screenshot, because there was no output. The job hung for 8-plus minutes and never produced a result, so I abandoned it (no credits charged). This is the first of three prompts that ask for &lt;em&gt;multiple independent objects in a spatial layout&lt;/em&gt; rather than a single connected form. This one simply hung.&lt;/p&gt;

&lt;h3&gt;
  
  
  P4 — functional compound form: gatehouse (best result)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Build a gatehouse with two towers and a central gate players can walk through."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxn10qgjf5h022ifiykns.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxn10qgjf5h022ifiykns.jpg" alt="Gatehouse result" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The strongest output in the session, and an important counter-example. It reads clearly as a castle gatehouse: two flanking towers, a central arch, battlements. Done in about a minute. Crucially, the functional requirement — "players can walk through" — was honored: the central gate is genuinely passable (verified by walking through it in world). The material came out stone-like, which fits the canonical "castle" prior, but the prompt did not specify a material, so that's a visual/prior win, not constraint-following.&lt;/p&gt;

&lt;p&gt;P4 sharpens the conclusion. A gatehouse has named sub-parts (two towers plus a gate) yet still generated fast and well, because it is &lt;strong&gt;one cohesive, canonical form&lt;/strong&gt; — unlike the market scene of separate objects.&lt;/p&gt;

&lt;h3&gt;
  
  
  S2 — multi-object scene: houses + path + trees
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Three small houses arranged in a row along a dirt path, with a tree between each house."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6uyyj1bey7fndbgcqjw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6uyyj1bey7fndbgcqjw.jpg" alt="S2 scene, aerial" width="800" height="517"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhr64irmco6j5iy0qwznb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhr64irmco6j5iy0qwznb.jpg" alt="S2 scene, ground" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Done in about two minutes, so scene prompts do not always hang — the market timeout looks like a one-off. And it genuinely emitted the scene &lt;em&gt;elements&lt;/em&gt;: dirt paths, several separate small structures, trees. But the composition is incoherent: wildly inconsistent scale (one oversized house next to a miniature cluster), scattered placement, blobby objects. It read the scene as one mesh to voxelize, not as an arrangement of objects. So "it cannot do scenes" is too strong; "it does not compose scenes coherently" is accurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 — multi-object scene: campsite
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"A campsite with two tents, a central campfire, and logs around it to sit on."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fninb6w95wtc7emcbyg48.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fninb6w95wtc7emcbyg48.jpg" alt="S3 campsite" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The opposite failure mode from S2. Instead of scattering, it collapsed into a single mound of teal blocks (the two tents merged into one), with a small patch of orange blocks that loosely reads as a campfire. No distinguishable tents, no logs. Elements are hinted by &lt;em&gt;color&lt;/em&gt; (tents teal, fire orange) but the arrangement is gone.&lt;/p&gt;

&lt;p&gt;Across the three scene prompts, none produced a coherent, usable multi-object layout. They failed three different ways: hang, scatter, collapse. The scene &lt;em&gt;elements&lt;/em&gt; can show up; the &lt;em&gt;composition&lt;/em&gt; did not.&lt;/p&gt;

&lt;h3&gt;
  
  
  P5 — negative constraint: 8×8 base, forbidden materials
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Build a tiny 8 by 8 starter base. Do not use glass, lava, water, or redstone."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9hfwi6xdr3aej7jx479.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9hfwi6xdr3aej7jx479.jpg" alt="P5 starter base" width="800" height="517"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjj33yt9f4spsz9tsxfoo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjj33yt9f4spsz9tsxfoo.jpg" alt="P5 top-down, footprint far larger than 8×8" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two findings. First, the &lt;em&gt;positive&lt;/em&gt; constraint failed the same way P1 and P2 did: the output is a large purple-orange slab, nowhere near 8×8, and not a "base." Second, the &lt;em&gt;negative&lt;/em&gt; constraint. Regions that look like lava or water turned out, on an in-game F3 block check, to be solid color-matched blocks. The orange band reads as &lt;code&gt;minecraft:orange_concrete&lt;/code&gt; with targeted fluid empty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwapelgyv0kvtwllagh4u.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwapelgyv0kvtwllagh4u.jpg" alt="In-game F3 block check on the lava-colored band" width="800" height="517"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;F3 debug, crosshair on the lava-colored band: &lt;code&gt;Targeted Block: minecraft:orange_concrete&lt;/code&gt;, &lt;code&gt;Targeted Fluid: minecraft:empty&lt;/code&gt; — a solid block chosen by color, not a forbidden material.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So no forbidden material was actually placed, but that's because the voxelizer's palette is solid colored blocks and it never places fluids or functional blocks, not because it parsed and honored "do not use." The negative constraint is met &lt;strong&gt;vacuously&lt;/strong&gt;, by palette limitation, not by rule-following.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's strong vs. weak
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strong:&lt;/strong&gt; single cohesive 3D forms with a clear visual prior — sphere, tower, castle gatehouse. Fast (about a minute), recognizable, and it will render named sub-parts (two towers, a gate, battlements) and even a functional opening (a walkable gate). Material is sensible when the canonical form implies it (castle implies stone).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weaker:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discrete constraints get dropped.&lt;/strong&gt; Exact dimensions ("15×15", "8×8"), specified materials ("wood and stone"), and positions ("door on the south side") did not come through. (P1, P2, P5)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-object scene composition (n=3, none coherent).&lt;/strong&gt; Across three scene prompts, none produced a coherent, usable arrangement: one hung, one scattered into inconsistent-scale fragments, one collapsed into a single blob. The elements can appear; the composition did not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative constraints are not meaningfully enforced.&lt;/strong&gt; Regions that looked like lava or water were solid color-matched blocks with no fluid. Forbidden materials are avoided by palette limitation, not by following the rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No validation signal.&lt;/strong&gt; The workflow surfaced no self-check or score for whether it met size, material, door, or function.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Across eight prompts a consistent line emerges: single cohesive forms are handled well, while scenes of independent objects and discrete or negative constraints are not. That's exactly what you'd expect from a mesh-to-voxel pipeline that generates one shape and color-maps it to blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you're using it
&lt;/h2&gt;

&lt;p&gt;If you want a quick, recognizable hero object — a tower, a statue-ish form, a gatehouse — Higgsfield's prompt-to-build is genuinely useful and fast. Lean into canonical shapes and let it pick the material.&lt;/p&gt;

&lt;p&gt;If you need a build to &lt;em&gt;satisfy&lt;/em&gt; something — an exact footprint, a specific material, a door where you asked for one, a multi-building scene with sensible spatial relationships — it isn't there yet in my samples. The gap isn't shape quality; it's everything layered on top of shape: constraints, composition, function, and any signal that the output actually met the ask. Treat the output as a starting silhouette to edit, not a finished, spec-correct build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scene composition is n=3.&lt;/strong&gt; Enough to say it produced no coherent scene in any try, but still a small sample.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One trial per prompt.&lt;/strong&gt; Generation is stochastic; I did not sample variance, and a given prompt might do better on a re-roll.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single rater&lt;/strong&gt;, scoring from screenshots plus a walkthrough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit-limited session&lt;/strong&gt; (started with 10 credits, about 1.15 per build), so two planned prompts went unrun.&lt;/li&gt;
&lt;li&gt;Figures are frames pulled from a daytime screen recording (the in-game screenshot key conflicted with macOS during the live session). The builds are unchanged; only lighting and clarity differ from the live run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest one-line takeaway: in this session Higgsfield's Minecraft prompt-to-build handled &lt;em&gt;shape&lt;/em&gt; well and &lt;em&gt;everything that makes a shape correct&lt;/em&gt; poorly. If you test it yourself, the fastest way to see the split is to run one canonical single form (a gatehouse) and one constrained one (a 15×15 cottage with a south door) back to back.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gamedev</category>
      <category>machinelearning</category>
      <category>review</category>
    </item>
    <item>
      <title>Agent Architecture Is a Compute Allocation Problem: The Advisor Strategy, Cost-Curve Frame Recursed</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Tue, 16 Jun 2026 20:14:53 +0000</pubDate>
      <link>https://dev.to/harrisonsec/agent-architecture-is-a-compute-allocation-problem-the-advisor-strategy-cost-curve-frame-recursed-d34</link>
      <guid>https://dev.to/harrisonsec/agent-architecture-is-a-compute-allocation-problem-the-advisor-strategy-cost-curve-frame-recursed-d34</guid>
      <description>&lt;p&gt;In April 2026, Anthropic published a blog post called &lt;em&gt;"The advisor strategy: Give agents an intelligence boost"&lt;/em&gt;, naming a pattern they had been A/B-testing in production: a cheaper model runs the agent loop end-to-end, an expensive model is consulted only when the cheap one hits a decision it can't solve. They reported concrete numbers — Haiku + Opus advisor on BrowseComp at 41.2% (Haiku alone: 19.7%) at 15% of the cost of running Sonnet through the whole task.&lt;/p&gt;

&lt;p&gt;On May 18, 2026, Tobi Lutke (CEO of Shopify) tweeted about an autoresearch setup that did exactly this: Qwen 3.6 27B running locally on an RTX 6000, with a small "advisor extension" that periodically calls GPT-5.5 for direction. 13,000 impressions, 2,400 likes, dozens of replies from engineers reproducing the pattern or building open-source implementations within hours.&lt;/p&gt;

&lt;p&gt;Underneath both of those, Stanford HazyResearch's &lt;a href="https://arxiv.org/abs/2512.21720" rel="noopener noreferrer"&gt;Minions&lt;/a&gt; paper — published months earlier — had abstracted the same pattern into a compressor-predictor framework: a small local model distills raw context into compact text that a larger remote model then reasons over. They reported their Deep Research system recovering 99% of frontier-model accuracy at 26% of the API cost.&lt;/p&gt;

&lt;p&gt;Three independent threads converging on the same architecture in roughly the same six-month window. That convergence is the story.&lt;/p&gt;

&lt;p&gt;This post argues something specific about it: the advisor strategy isn't a new pattern invented for LLMs. It's the &lt;strong&gt;third recursion&lt;/strong&gt; of the cost-curve frame from earlier in this mini-series — the same idea that argued grep beats RAG for code retrieval, and that argued SQLite + FTS5 beats a vector DB for the symbol-graph storage that grep-replacement tools (CodeGraph) need. Applied at the model-orchestration layer, the frame produces the advisor strategy. The strategy is the architecture; the frame is why.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — Anthropic, Tobi Lutke, and HazyResearch independently shipped (or described) the same agent pattern in early 2026: a cheap model runs the loop, an expensive model is consulted only for decisions. The convergence is evidence the pattern is correct; the &lt;em&gt;reason&lt;/em&gt; it's correct is the cost-curve frame from this series' first post, applied at the model-choice layer instead of the retrieval-architecture layer. Piece B argued grep+loop beats RAG because build/maintain cost dominates per-query cost below a crossover. The advisor strategy argues the same shape for tokens: cheap-model executor cost dominates expensive-model advisor cost for the bulk of low-value operations (reading context, format conversion, retries), so expensive-model tokens should be spent only at high-value decision points. Same frame, third layer.&lt;/p&gt;

&lt;p&gt;The post does three things: (1) reports the three converging threads with what each contributed; (2) makes the cost-curve recursion argument explicitly — L1 retrieval, L2 storage, L3 model orchestration; (3) maps the gotchas the hype skips (data egress on handoff, eval difficulty, handoff-contract design as actual engineering, hardware realism). The mini-series concludes here, five posts in, with cost-curve frame as a meta-design law across three layers of agent architecture.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Three convergent threads, in the order they shipped
&lt;/h2&gt;

&lt;p&gt;The convergence matters more than any single thread. Each was independent; each shipped within a six-month window of the others; each describes the same architecture from a different vantage. That's how you know the pattern is real and not just one team's design preference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anthropic's official advisor strategy (2026-04-09)
&lt;/h3&gt;

&lt;p&gt;The Anthropic engineering blog &lt;em&gt;"The advisor strategy: Give agents an intelligence boost"&lt;/em&gt; defines the pattern as a productized engineering primitive:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Sonnet or Haiku runs the task end-to-end as the executor... When the executor hits a decision it can't reasonably solve, it consults Opus for guidance as the advisor."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The advisor never calls tools or produces user-facing output, and only provides guidance to the executor."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reported empirical numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Cost (relative to Sonnet end-to-end)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet alone (no advisor)&lt;/td&gt;
&lt;td&gt;SWE-bench Multilingual&lt;/td&gt;
&lt;td&gt;(baseline)&lt;/td&gt;
&lt;td&gt;1.00×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet + Opus advisor&lt;/td&gt;
&lt;td&gt;SWE-bench Multilingual&lt;/td&gt;
&lt;td&gt;exceeds baseline&lt;/td&gt;
&lt;td&gt;0.88× (−11.9%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku alone&lt;/td&gt;
&lt;td&gt;BrowseComp&lt;/td&gt;
&lt;td&gt;19.7%&lt;/td&gt;
&lt;td&gt;(baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku + Opus advisor&lt;/td&gt;
&lt;td&gt;BrowseComp&lt;/td&gt;
&lt;td&gt;41.2%&lt;/td&gt;
&lt;td&gt;0.15× of Sonnet-end-to-end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two observations on the numbers. First: the Sonnet + Opus combination outperforms Sonnet alone while &lt;em&gt;also&lt;/em&gt; being cheaper — that's not a one-axis trade, that's a Pareto improvement. Second: the Haiku + Opus combination doubles Haiku's standalone score while costing 15% of Sonnet's. That's the &lt;em&gt;compound&lt;/em&gt; gain — better and cheaper at the same time.&lt;/p&gt;

&lt;p&gt;A specific detail in the blog: the advisor's outputs are typically &lt;strong&gt;400–700 tokens&lt;/strong&gt; — a short plan, not a full solution. That's the design saying out loud what the cost curve implies — the advisor exists to redirect, not to do work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tobi Lutke's personal experiment (2026-05-18)
&lt;/h3&gt;

&lt;p&gt;Tobi Lutke (CEO of Shopify) posted on X:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I've had very good results running autoresearch with local qwen 3.6 26b model as long as I had a simple vibed pi 'advisor' extension that allowed it to periodically ask GPT 5.5 for ideas. I think this direction has a lot of merit."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tobi's setup is the open-source mirror of Anthropic's productized pattern, with two architectural variants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Locality:&lt;/strong&gt; the executor runs on his own hardware (Qwen 3.6 27B on an RTX 6000), not on Anthropic's API. Local-first by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontier model choice:&lt;/strong&gt; the advisor is GPT-5.5 (OpenAI), not Opus (Anthropic). The pattern is model-agnostic on the advisor side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hardware caveat is real and worth naming: RTX 6000 is professional-grade, not consumer hardware. 27B-dense models with autoresearch-length contexts aren't laptop workloads. The pattern is reproducible at the &lt;em&gt;architecture&lt;/em&gt; level on commodity infrastructure; the &lt;em&gt;specific&lt;/em&gt; setup Tobi shows takes real investment.&lt;/p&gt;

&lt;p&gt;Within hours of Tobi's tweet, developer Rob Zolkos published &lt;a href="https://github.com/robzolkos/pi-lifeline" rel="noopener noreferrer"&gt;&lt;code&gt;pi-lifeline&lt;/code&gt;&lt;/a&gt; — an open-source escalation extension explicitly inspired by the tweet, with reasonable defaults: at least 5 rounds before the first advisor call, automatic escalation after 3 consecutive failures, plateau-detection after 6 rounds, max 10 advisor calls per session, default advisor model GPT-5.5. That's &lt;em&gt;engineering&lt;/em&gt; of the handoff contract — not a one-line config — and we'll come back to it later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stanford HazyResearch Minions (2025–2026 publication window)
&lt;/h3&gt;

&lt;p&gt;Linked from a reply on Tobi's tweet — Dan Biderman pointing at HazyResearch's &lt;a href="https://arxiv.org/abs/2512.21720" rel="noopener noreferrer"&gt;Minions paper&lt;/a&gt; (arXiv 2512.21720), which abstracts the pattern into a &lt;em&gt;compressor-predictor framework&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"smaller 'compressor' LMs (that can even run locally) distill raw context into compact text that is then consumed by larger 'predictor' LMs."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Minions paper's specific numerical contribution: in their Deep Research system, a local 3B-parameter compressor recovers 99% of frontier-model accuracy at 26% of the API cost. That's the &lt;strong&gt;academic version&lt;/strong&gt; of the same architecture, with empirical bounds.&lt;/p&gt;

&lt;p&gt;Three things HazyResearch's framing adds beyond Anthropic's product blog:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The compressor doesn't have to be 27B&lt;/strong&gt; — even 3B works for context distillation, depending on the task. The lower the compressor can go, the more local you can run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost-recovery curve has a specific shape&lt;/strong&gt; — 99% accuracy at 26% cost isn't linear. It's the same Pareto improvement Anthropic reported in product form: better and cheaper, not just cheaper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The general framing is "compress then decide"&lt;/strong&gt; — a slightly broader frame than "executor + advisor" because it includes the case where the compressor runs once at the start and the predictor runs once at the end, with no escalation loop. The advisor strategy is a &lt;em&gt;streaming&lt;/em&gt; version of compress-then-decide where compression happens iteratively.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why three independent confirmations matter
&lt;/h3&gt;

&lt;p&gt;Each thread is from a different vantage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic&lt;/strong&gt;: product engineering. Owns the model, designed the workload, reports field metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tobi Lutke&lt;/strong&gt;: individual practitioner. Different model providers (Qwen + GPT-5.5), different hosting (local + cloud), different workload (autoresearch, not coding benchmarks). Reproduced the pattern without coordinating with Anthropic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HazyResearch&lt;/strong&gt;: academic research. Different framing (compressor-predictor), different time horizon (paper preceded Anthropic's blog), different cost-quality measurement methodology.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When three independent vantages produce the same architectural answer, the design is robust to who happens to be sponsoring the work. That's the convergence-as-evidence argument — &lt;em&gt;the pattern is real and not just downstream of one organization's preferences&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The interesting question now isn't whether the pattern works (the convergence proves it does). It's &lt;strong&gt;why&lt;/strong&gt; it works — and that question has a clean answer from earlier in this mini-series.&lt;/p&gt;




&lt;h2&gt;
  
  
  The cost-curve recursion: same frame, third layer
&lt;/h2&gt;

&lt;p&gt;Piece B (the first post in this series) argued that LLM-driven code retrieval sits on a &lt;em&gt;cost curve&lt;/em&gt;: index-based approaches pay high build cost + super-linear maintain cost, tool-loop approaches pay per-query cost only. Below a crossover point — which sits well above most projects' size — tool-loops win. Above it, indexes pay back.&lt;/p&gt;

&lt;p&gt;That argument generalizes. Applied to other agent-architecture decisions, the same frame keeps producing the right call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1 — Retrieval architecture (Piece B)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tool-loop (grep + LLM iteration)&lt;/th&gt;
&lt;th&gt;Index (vector RAG)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build cost&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;super-linear in repo size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintain cost&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;super-linear in churn × structural complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-query cost&lt;/td&gt;
&lt;td&gt;N tool-call round-trips&lt;/td&gt;
&lt;td&gt;one vector search + LLM reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Win condition&lt;/td&gt;
&lt;td&gt;Below crossover&lt;/td&gt;
&lt;td&gt;Above crossover&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Conclusion: for most repos, build/maintain cost dominates per-query savings, so tool-loop wins. Anthropic chose grep+Glob+Read for Claude Code, not an index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2 — Index storage (C2, the first-principles read of CodeGraph)
&lt;/h3&gt;

&lt;p&gt;When you &lt;em&gt;do&lt;/em&gt; cross the curve and need an index — CodeGraph's territory — the next decision is which storage layer to use.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;FTS5 + SQLite (CodeGraph)&lt;/th&gt;
&lt;th&gt;Vector DB (Chroma / Pinecone)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build cost&lt;/td&gt;
&lt;td&gt;linear in source size, parse-only&lt;/td&gt;
&lt;td&gt;super-linear (chunk + embed every file)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintain cost&lt;/td&gt;
&lt;td&gt;low (file watcher + incremental parse)&lt;/td&gt;
&lt;td&gt;super-linear (re-embed on change, handle cross-chunk refs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-query cost&lt;/td&gt;
&lt;td&gt;exact lookup, sub-millisecond&lt;/td&gt;
&lt;td&gt;ANN search + rerank + LLM call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Win condition&lt;/td&gt;
&lt;td&gt;Exact-lookup workload&lt;/td&gt;
&lt;td&gt;Semantic-similarity workload&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CodeGraph's queries are exact lookups (find symbol X, trace A→B, callers of Y), so FTS5 wins. Same frame as Layer 1: pay only the costs your workload demands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3 — Model orchestration (this post — the advisor strategy)
&lt;/h3&gt;

&lt;p&gt;Apply the same frame to token allocation across models.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Cheap-only (Haiku alone, Qwen alone)&lt;/th&gt;
&lt;th&gt;Expensive-only (Sonnet/Opus end-to-end)&lt;/th&gt;
&lt;th&gt;Executor + Advisor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per-token cost on bulk operations&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;low (cheap executor handles 90%+ of tokens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-token cost on key decisions&lt;/td&gt;
&lt;td&gt;low (but quality suffers)&lt;/td&gt;
&lt;td&gt;high (and quality matches)&lt;/td&gt;
&lt;td&gt;high (advisor only for decision tokens, ~400–700 tokens per call)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate task cost&lt;/td&gt;
&lt;td&gt;low if quality holds&lt;/td&gt;
&lt;td&gt;high regardless&lt;/td&gt;
&lt;td&gt;low (most tokens are cheap; decision tokens compound from the expensive model's quality)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate task quality&lt;/td&gt;
&lt;td&gt;depends on whether decisions are within cheap model's capability&lt;/td&gt;
&lt;td&gt;full&lt;/td&gt;
&lt;td&gt;high (cheap executor + expensive decisions ≈ expensive end-to-end, sometimes better)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Win condition&lt;/td&gt;
&lt;td&gt;Tasks where cheap model alone is adequate&lt;/td&gt;
&lt;td&gt;Tasks where any decision could be critical&lt;/td&gt;
&lt;td&gt;Tasks where most operations are routine but some decisions are hard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most agent tasks fit the last column. The advisor strategy wins for the same structural reason grep+loop wins at Layer 1: &lt;strong&gt;the cost of the "bulk" operations dominates the cost of the "decision" operations, so the architecture should put the cheap tool on the bulk path and reserve the expensive tool for the decision path.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost-curve as a meta-design frame
&lt;/h3&gt;

&lt;p&gt;Stating the generalization explicitly: &lt;strong&gt;whenever an architecture has a "many low-value operations + few high-value operations" structure, applying expensive tools uniformly across both pays the high cost for the low-value operations too. The right design separates the two paths and uses cheap-but-good-enough tools on the bulk path.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the design rule the cost-curve frame produces at every layer it's been applied to in this series. It's not specific to LLMs — &lt;em&gt;the database community calls this "use the cheapest index that satisfies the query class"; the systems community calls this "tiered storage"; the chip design community calls this "the memory hierarchy".&lt;/em&gt; The LLM-engineering version is the advisor strategy plus its retrieval-architecture cousins.&lt;/p&gt;

&lt;p&gt;This is the meta-design law the five posts in this series argue for. The argument's strength comes from the convergence — three independent recursions of the same frame producing the right architecture each time. That's not coincidence; it's the frame doing its job.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this validates from Piece B's source-code analysis
&lt;/h2&gt;

&lt;p&gt;Piece B's analysis of Claude Code's source code reported a specific finding: the Explore subagent runs on Haiku for non-ant builds (external users), not on Sonnet or Opus. The reasoning section of Piece B observed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Explore runs on Haiku for external users. Not the main reasoning model. Exploration is a cheap-tokens job — there's no creative reasoning happening, just iterate-and-filter — and Anthropic uses a fast, small, cheap model for it. The main agent gets the expensive model when it gets the summary back. This is the staffing analogue: junior associate does the deposition review, senior partner reads the brief."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the advisor strategy, visible directly in Claude Code's source. Piece B analyzed the mechanism rather than the branding, so it didn't use Anthropic's later label — but it's the same architecture. The point isn't priority over the announcement; it's that the pattern was already running in shipped code, observable by anyone reading the source rather than waiting for a launch post to name it.&lt;/p&gt;

&lt;p&gt;There's a useful takeaway here for reading any AI engineering work: &lt;strong&gt;the source code is ahead of the blog posts&lt;/strong&gt;. The blog post explains and packages what's been running in production. Reading the source is one of the cheapest ways to see where the foundational labs are betting, because the explainer post usually describes what was already shipping in the code months earlier.&lt;/p&gt;

&lt;p&gt;The advisor strategy is one of three patterns Piece B's source-code reading surfaced in this category. The other two are worth flagging because they suggest the &lt;em&gt;next&lt;/em&gt; blog posts to expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Fork-subagent architecture&lt;/strong&gt; (visible behind the &lt;code&gt;isForkSubagentEnabled()&lt;/code&gt; flag) — a different model-orchestration shape where the cheap and expensive halves &lt;em&gt;share&lt;/em&gt; a context (and prompt cache) rather than separating cleanly. If Anthropic productizes this, expect a blog post titled something like "Fork: shared-context model collaboration" in the next 1–3 quarters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;tengu_amber_stoat&lt;/code&gt; GrowthBook flag&lt;/strong&gt; — gating Explore vs. no-Explore as a deeper architectural test. If Anthropic concludes the cheap-executor-as-separate-subagent pattern doesn't pay off, the next blog post is about &lt;em&gt;why&lt;/em&gt; the advisor strategy works in some shapes and not others.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general point: &lt;strong&gt;reading the source code and observing the patterns lets you write the analysis before the productized name arrives.&lt;/strong&gt; When the name does arrive, your analysis is what frames it. This is the time-shift advantage that source-leaning engineering writing has over pure-press-release-paraphrase content. It's why this series' posts have been holding up under fresh data — the frame was built from the same source the announcements describe, so new announcements tend to confirm it rather than surprise it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The gotchas the hype skips
&lt;/h2&gt;

&lt;p&gt;The convergence between Tobi, Anthropic, and HazyResearch is real and the pattern is solid. But there are four gotchas the hype reliably skips that any production implementation has to address.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Data egress on handoff
&lt;/h3&gt;

&lt;p&gt;The local-first appeal of Tobi's setup (executor runs on your own GPU) hides a subtle leak. &lt;strong&gt;Every time the executor escalates to the cloud-hosted advisor, some subset of the executor's context goes to the advisor's hosting environment.&lt;/strong&gt; What gets sent is the executor's choice; once it's sent, it's no longer local.&lt;/p&gt;

&lt;p&gt;Commenter &lt;code&gt;@DarshanSays&lt;/code&gt; on Tobi's tweet flagged this explicitly: &lt;em&gt;"local + remote advisor mode quietly creates a data egress channel."&lt;/em&gt; The pattern gives you &lt;em&gt;cost control&lt;/em&gt; and &lt;em&gt;partial privacy&lt;/em&gt; — most of your raw data stays local — but not &lt;em&gt;full privacy&lt;/em&gt;. For workloads on sensitive data (security tooling, healthcare records, internal source code), the advisor's contract is now an exfiltration vector if it's poorly designed.&lt;/p&gt;

&lt;p&gt;The mitigations are real engineering, not config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redaction layer&lt;/strong&gt; between executor and advisor — strip identifiers, replace specific names with placeholders, summarize before sending&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-off contract documentation&lt;/strong&gt; — explicit specification of what gets sent and what's excluded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logging&lt;/strong&gt; — every advisor call is logged with what was sent, so it's reviewable&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Eval is structurally harder than it looks
&lt;/h3&gt;

&lt;p&gt;The benchmark numbers Anthropic and HazyResearch reported are real but represent specific tasks. &lt;strong&gt;For your task, you don't know whether the advisor strategy pays off without measuring on your workload.&lt;/strong&gt; And measuring is harder than for a single-model agent because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The executor's failure modes and the advisor's failure modes interact — bad escalation can make the advisor strategy worse than executor-alone&lt;/li&gt;
&lt;li&gt;The right escalation policy is task-dependent — too eager wastes advisor cost, too reluctant leaves executor stuck&lt;/li&gt;
&lt;li&gt;Quality differences from advisor strategy show up not just in pass/fail but in answer &lt;em&gt;completeness&lt;/em&gt; (similar to the Q4 refactor-impact analysis in C1's benchmark) and &lt;em&gt;modal status&lt;/em&gt; (is the answer correctly hedged vs. confidently wrong? — see Piece A's modality-flattening discussion)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A serious eval setup for the advisor strategy needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Baseline: executor alone, expensive model alone, advisor-strategy variant — three arms, not two&lt;/li&gt;
&lt;li&gt;Multiple escalation policies (eager / moderate / conservative) tested separately&lt;/li&gt;
&lt;li&gt;Both correctness &lt;em&gt;and&lt;/em&gt; completeness scoring, not just pass/fail&lt;/li&gt;
&lt;li&gt;Statistical reporting (variance across runs, not just averages)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more work than benchmarking a single model. It's the kind of thing teams skip because the "single number" benchmarks already look good — but the single numbers can hide that the &lt;em&gt;policy&lt;/em&gt; matters more than the &lt;em&gt;configuration&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Hand-off contract design is real engineering
&lt;/h3&gt;

&lt;p&gt;The advisor strategy's "magic" is the executor calling the advisor at the right moments with the right context and getting back actionable guidance. Every clause in that sentence hides an engineering decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When to escalate&lt;/strong&gt; — after N consecutive failures? When confidence (measured how?) drops below threshold? After K rounds of no progress?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What context to send&lt;/strong&gt; — the full executor working state? A compressed summary? The recent N actions and outcomes?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to format the advisor's response&lt;/strong&gt; — free-form text? Structured JSON? Action recommendations vs. analysis?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How the executor integrates the advice&lt;/strong&gt; — adopt verbatim? Treat as a hint? Use to seed the next attempt?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pi-lifeline's defaults (5 rounds before first advisor call, 3 consecutive failures auto-escalates, plateau-detection at 6 rounds, max 10 advisor calls per session) are one set of choices. They're reasonable but not universal. &lt;strong&gt;The right choices depend on the task; getting them wrong destroys the strategy's value even when the underlying models are good.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Hardware realism
&lt;/h3&gt;

&lt;p&gt;Tobi runs Qwen 3.6 27B on an RTX 6000 (NVIDIA professional-grade). The published benchmarks from Anthropic and HazyResearch use specific model versions and infrastructure. &lt;strong&gt;The architecture is reproducible on commodity infrastructure; the specific results are not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For practitioners considering the pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local executor (Qwen-class 27B+ dense model with long-context autoresearch loads) realistically needs RTX 6000 or A100-class hardware. Consumer cards (RTX 4090, RTX 5090) work for shorter contexts but throughput drops on long sessions.&lt;/li&gt;
&lt;li&gt;Quantized GGUF versions (e.g., Unsloth's quantizations) help with VRAM but not throughput — same hardware needed for the same wall-clock latency&lt;/li&gt;
&lt;li&gt;Hybrid cloud-first executor (Haiku/Sonnet on Anthropic API) avoids the hardware question but loses the local-data-leaves-only-on-escalation property&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The realistic deployment shape depends on what you're trying to optimize. Cost-only: cheap cloud executor + expensive cloud advisor. Privacy-first: local executor + cloud advisor with redaction. Speed-first: cloud executor with low advisor latency. The advisor-strategy &lt;em&gt;architecture&lt;/em&gt; is the constant; the &lt;em&gt;implementation&lt;/em&gt; varies by which axis dominates your requirements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engineering implementation: where to actually start
&lt;/h2&gt;

&lt;p&gt;If you're considering the advisor strategy on a real workload, the cheapest first step is to &lt;strong&gt;measure your single-model agent's token distribution&lt;/strong&gt; — what percentage of tokens go to context reading vs. format conversion vs. actual reasoning. If 70%+ of tokens go to bulk operations, the advisor strategy has a big payoff potential. If the distribution is flatter, payoff is smaller and the engineering overhead may not be worth it.&lt;/p&gt;

&lt;p&gt;Once you've decided to try the pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick an executor model&lt;/strong&gt; — start with the cheapest model that can complete &lt;em&gt;most&lt;/em&gt; of your tasks reliably. For coding agents, Haiku is the obvious starting point; for autoresearch, Qwen 3.6 27B (or whatever local 27B-class model fits your hardware).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick an advisor model&lt;/strong&gt; — Opus for Anthropic-stack workloads, GPT-5.5 for OpenAI-stack, whichever frontier model you trust on the task class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design escalation triggers&lt;/strong&gt; — start with pi-lifeline's defaults as baseline, tune based on observed executor failure patterns. The right number depends on your task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design the hand-off contract&lt;/strong&gt; — what context goes to the advisor, what format the advisor returns. Start minimal (recent N actions + current goal), expand if advisor quality is poor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement redaction&lt;/strong&gt; — if your data is sensitive, the redaction layer is non-negotiable. If not, you can skip it for v0 but document the egress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure&lt;/strong&gt; — three-arm eval (executor alone, advisor strategy, expensive alone), correctness + completeness scoring, variance across runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate on the policy, not the models&lt;/strong&gt; — when the strategy underperforms, the fix is usually in escalation timing or hand-off content, not in swapping models.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The pattern works. The engineering around it determines whether it works &lt;em&gt;for you&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing — the mini-series, completed
&lt;/h2&gt;

&lt;p&gt;This is the fifth and final post in a series on agent retrieval, memory, and orchestration architectures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;&lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;Agent Retrieval Is a Cost Curve Problem&lt;/a&gt;&lt;/em&gt; (2026-05-25) — Layer 1: retrieval architecture. Why grep+loop beats RAG for code, and why the cost curve says so.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;&lt;a href="https://harrisonsec.com/blog/agent-memory-is-a-cache-coherence-problem/" rel="noopener noreferrer"&gt;Agent Memory Is a Cache Coherence Problem&lt;/a&gt;&lt;/em&gt; (2026-05-28) — the cache-coherence frame for lossy auto-capture AI memory tools, with the modality-flattening failure mode mapped out.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;&lt;a href="https://harrisonsec.com/blog/i-tested-codegraph-on-hono-benchmark/" rel="noopener noreferrer"&gt;I Tested CodeGraph on Hono. The Tool-Call Savings Reproduce — the Cost Savings Don't.&lt;/a&gt;&lt;/em&gt; (2026-06-01) — empirical: when the cost curve &lt;em&gt;is&lt;/em&gt; crossed (mid-size repo, architectural questions, static-typed language), CodeGraph crosses it cheaply.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;&lt;a href="https://harrisonsec.com/blog/codegraph-architecture-first-principles-llm-retrieval/" rel="noopener noreferrer"&gt;Agent Retrieval Above the Crossover: A First-Principles Read of CodeGraph&lt;/a&gt;&lt;/em&gt; (2026-06-08) — Layer 2: index storage. Why SQLite + FTS5 beats vector DBs for the symbol-graph workload, and where CodeGraph's abstraction leaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This post&lt;/strong&gt; (2026-06-15) — Layer 3: model orchestration. The advisor strategy as the third recursion of the cost-curve frame, validated by Anthropic's product, Tobi Lutke's experiment, and HazyResearch's academic version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read as one argument&lt;/strong&gt;: the same cost-curve frame applies at three layers of agent architecture. At each layer, the correct design separates "bulk operations" from "decision operations" and pays only the cost each operation class requires. The five posts are five different applications of one frame, each cross-checked against fresh data as the productized announcements landed — the frame held up because it was built from the same shipped source those announcements describe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read as a toolkit&lt;/strong&gt;: if you're designing or evaluating agent architectures, the question to ask at every layer is the same. &lt;em&gt;What's the cost distribution of operations at this layer? Is there a "bulk vs. decision" split? Can the bulk path use a cheaper tool? Does the expensive tool only need to be on the decision path?&lt;/em&gt; Apply at retrieval (grep vs. RAG), storage (FTS5 vs. vector), model orchestration (executor vs. advisor). The next layer the frame will apply to is plausibly memory consolidation (cheap distillation vs. expensive synthesis) — that's a future post topic if the pattern shows up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on L2 iterating fast&lt;/strong&gt; — between when this mini-series started (B published 2026-05-25) and when D publishes (2026-06-15), the LLM-symbol-graph layer kept moving: CodeGraph shipped point releases, and more tools in the same class are arriving. They all hit the same six conditions for a viable LLM-symbol-graph that the framework predicted; where they differ is inside the &lt;em&gt;ranking&lt;/em&gt; layer — how each one orders the symbols a query surfaces (keyword + heuristics, graph-walk propagation, embedding re-rank, whatever comes next). The six conditions are about &lt;em&gt;what's required for an LLM-symbol-graph to exist at all&lt;/em&gt;; the ranking algorithm is the secondary design space &lt;em&gt;within&lt;/em&gt; the framework, and it's where the next tool will try to win. The empirical read of CodeGraph on a repo its team didn't choose is in the &lt;a href="https://harrisonsec.com/blog/i-tested-codegraph-on-hono-benchmark/" rel="noopener noreferrer"&gt;companion benchmark post&lt;/a&gt;; the first-principles architectural read is in the &lt;a href="https://harrisonsec.com/blog/codegraph-architecture-first-principles-llm-retrieval/" rel="noopener noreferrer"&gt;companion Lab post&lt;/a&gt;. L2 keeps iterating; the framework is what's stable.&lt;/p&gt;

&lt;p&gt;Three threads converged on the advisor strategy because the cost-curve frame produced it independently each time. The frame is the durable insight; the architecture is the frame instantiated at one layer. Reading the source code, watching the productization, and modeling the convergence each contribute to the same picture.&lt;/p&gt;

&lt;p&gt;If you build agents and are paying frontier-model rates for tokens that don't need them, the advisor strategy is the practical fix. If you read agents and want a frame for evaluating what comes next, the cost-curve recursion is the lens. The series ends here, five posts and three layers in.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Companion piece 1: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;Agent Retrieval Is a Cost Curve Problem: Why Claude Code Doesn't Use RAG&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;Companion piece 2: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/agent-memory-is-a-cache-coherence-problem/" rel="noopener noreferrer"&gt;Agent Memory Is a Cache Coherence Problem&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;Empirical pair (Operator track): *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/i-tested-codegraph-on-hono-benchmark/" rel="noopener noreferrer"&gt;I Tested CodeGraph on Hono. The Tool-Call Savings Reproduce — the Cost Savings Don't.&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;First-principles companion: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/codegraph-architecture-first-principles-llm-retrieval/" rel="noopener noreferrer"&gt;Agent Retrieval Above the Crossover: A First-Principles Read of CodeGraph&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;Background: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/consistency-scenarios-and-approaches-production/" rel="noopener noreferrer"&gt;Consistency in Distributed Systems: Scenarios, Trade-offs, and What Actually Works&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;Anthropic advisor strategy blog (2026-04-09): "The advisor strategy: Give agents an intelligence boost"&lt;/em&gt;&lt;br&gt;
&lt;em&gt;HazyResearch Minions paper: &lt;a href="https://arxiv.org/abs/2512.21720" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2512.21720&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;pi-lifeline (open-source escalation extension inspired by Tobi Lutke): &lt;a href="https://github.com/robzolkos/pi-lifeline" rel="noopener noreferrer"&gt;https://github.com/robzolkos/pi-lifeline&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Agent Retrieval Above the Crossover: A First-Principles Read of CodeGraph</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Mon, 08 Jun 2026 19:49:41 +0000</pubDate>
      <link>https://dev.to/harrisonsec/agent-retrieval-above-the-crossover-a-first-principles-read-of-codegraph-1gd7</link>
      <guid>https://dev.to/harrisonsec/agent-retrieval-above-the-crossover-a-first-principles-read-of-codegraph-1gd7</guid>
      <description>&lt;p&gt;The prior post in this series, &lt;em&gt;&lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;Agent Retrieval Is a Cost Curve Problem&lt;/a&gt;&lt;/em&gt;, argued that a viable LLM-symbol-graph would need to satisfy six specific conditions — and that no existing tool had hit all six. The post went live on 2026-05-25; seven days earlier, &lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;CodeGraph&lt;/a&gt; had hit GitHub trending with exactly those six properties satisfied.&lt;/p&gt;

&lt;p&gt;That's the easy version of the update: framework predicted it, someone shipped it, here's the existence proof. The companion piece (&lt;em&gt;&lt;a href="https://harrisonsec.com/blog/i-tested-codegraph-on-hono-benchmark/" rel="noopener noreferrer"&gt;I Tested CodeGraph on Hono. The Tool-Call Savings Reproduce — the Cost Savings Don't.&lt;/a&gt;&lt;/em&gt;) handles the empirical half — 40 verified-connected runs, a decision matrix, the install-or-not call. Short version of that post: the tool-call savings reproduce on an independent repo (−55%), the &lt;strong&gt;cost&lt;/strong&gt; savings from the vendor benchmark don't (+7% at Hono's size). Fewer steps, not fewer dollars, until your repo is big enough.&lt;/p&gt;

&lt;p&gt;This post is the harder version of the update.&lt;/p&gt;

&lt;p&gt;The interesting question isn't whether CodeGraph works. The interesting question is &lt;strong&gt;why are its specific architectural choices right, and where does the abstraction inevitably leak?&lt;/strong&gt; Answering it gives you the lens for evaluating the next CodeGraph-class tool that ships — and there will be many — without redoing the benchmark each time.&lt;/p&gt;

&lt;p&gt;To answer it concretely rather than abstractly, I read CodeGraph against its own artifact: the SQLite database it writes to &lt;code&gt;.codegraph/codegraph.db&lt;/code&gt;. Every structural claim below is checked against the index it actually built for Hono (CodeGraph v0.9.7: 362 files, 4,128 nodes, 8,225 edges, a 7.4 MB database). The schema turns out to be the clearest statement of the architecture the tool's README never makes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — CodeGraph's architecture is right for three reasons that aren't obvious from the feature list, and all three are visible in its SQLite schema. (1) &lt;strong&gt;The AST extraction boundary&lt;/strong&gt;: tree-sitter takes what &lt;em&gt;syntax&lt;/em&gt; tells you (4,128 nodes across 13 kinds, 8,225 edges across 7 kinds) and leaves the rest to the LLM. The boundary is literal — references syntax can't resolve go into an &lt;code&gt;unresolved_refs&lt;/code&gt; table instead of becoming fake edges. (2) &lt;strong&gt;SQLite + FTS5, not a vector DB&lt;/strong&gt;: the index is plain relational tables plus a full-text table over symbol names. Zero embedding columns. The queries are exact lookups that B-tree indexes answer in log time; vector search would be solving a harder problem the workload never asks. This is the prior post's cost curve, recursed onto the index tool itself. (3) &lt;strong&gt;The abstraction leaks where syntax diverges from runtime semantics&lt;/strong&gt; — macros, metaprogramming, codegen, JIT binding. CodeGraph tags its few guessed edges with a &lt;code&gt;heuristic&lt;/code&gt; provenance flag (7 of 8,225 on Hono), which is honest; but what tree-sitter can't see at all gets no edge and no flag. Knowing that boundary is what separates a tool you trust from one you cargo-cult.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why this is a first-principles question, not a tool review
&lt;/h2&gt;

&lt;p&gt;Most coverage of CodeGraph reads like &lt;em&gt;"19k stars in a week, here's the install script."&lt;/em&gt; That's news; it isn't analysis. The same coverage will get written for every CodeGraph-class tool that ships in the next 18 months, because the pattern — tree-sitter + local index + MCP server + an instruction snippet that routes the agent to it — is now demonstrated and the ingredients are well known.&lt;/p&gt;

&lt;p&gt;The durable question isn't &lt;em&gt;"is CodeGraph good?"&lt;/em&gt; It's &lt;em&gt;"what makes this class of tool architecturally correct, and how do I evaluate the next one?"&lt;/em&gt; That's what a first-principles read produces. The benchmark in the companion post is one data point; this post is the lens for reading all future data points in the same space.&lt;/p&gt;

&lt;p&gt;If you're deciding on CodeGraph specifically, read the companion. If you're thinking about LLM retrieval as a discipline — or about to bet on, or build, a similar tool — read this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap: the six conditions, in 30 seconds
&lt;/h2&gt;

&lt;p&gt;The prior post argued any viable LLM-symbol-graph needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No-compile parsing&lt;/strong&gt; — cold start in seconds, not minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language portability&lt;/strong&gt; — one binary for many languages, not one server per stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM-shaped API&lt;/strong&gt; — flat, recordy output the model can digest, not nested LSP hierarchies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broad enough coverage&lt;/strong&gt; — code-as-structure plus a text-search fallback for everything else&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live update without reindex&lt;/strong&gt; — file-watcher-driven, no manual rebuild&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-config install&lt;/strong&gt; — single binary, configures the agent automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CodeGraph hits all six (the field-by-field mapping is near the end of this post). Taking the mapping as established, the interesting move is to ask: &lt;strong&gt;of the design choices CodeGraph made to hit those six, which were forced and which could have gone the other way?&lt;/strong&gt; The forced ones are good engineering. The ones that &lt;em&gt;weren't&lt;/em&gt; forced — where CodeGraph picked something specific over a live alternative — are where the architecture is making a claim, and where the first-principles content lives.&lt;/p&gt;

&lt;p&gt;Three of those choices repay a deep read. The other three (file-watcher update, single-binary distribution, instruction-snippet routing) are well-understood in their own fields — OS notifications, package distribution, prompt engineering — and amount to "do the obvious thing well." The three that don't are the three this post takes apart, each against the actual index.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 1 — The AST extraction boundary: an information-theoretic case
&lt;/h2&gt;

&lt;p&gt;CodeGraph parses source with tree-sitter and extracts a specific subset of the syntax into its graph. You don't have to take the README's word for what that subset is — it's enumerable straight out of the &lt;code&gt;nodes&lt;/code&gt; and &lt;code&gt;edges&lt;/code&gt; tables. On Hono, the 4,128 nodes break down like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node kind&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Node kind&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;import&lt;/td&gt;
&lt;td&gt;1,033&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;method&lt;/td&gt;
&lt;td&gt;240&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;route&lt;/td&gt;
&lt;td&gt;873&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;interface&lt;/td&gt;
&lt;td&gt;187&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;function&lt;/td&gt;
&lt;td&gt;569&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;property&lt;/td&gt;
&lt;td&gt;169&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;file&lt;/td&gt;
&lt;td&gt;362&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;class&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;type_alias&lt;/td&gt;
&lt;td&gt;358&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;enum_member&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;constant&lt;/td&gt;
&lt;td&gt;247&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;variable / enum&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the 8,225 edges, which are the actually interesting part:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Edge kind&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;What it encodes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;contains&lt;/td&gt;
&lt;td&gt;2,874&lt;/td&gt;
&lt;td&gt;structural nesting (file → class → method)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;calls&lt;/td&gt;
&lt;td&gt;2,230&lt;/td&gt;
&lt;td&gt;the call graph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;references&lt;/td&gt;
&lt;td&gt;1,955&lt;/td&gt;
&lt;td&gt;symbol used here, defined there&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;imports&lt;/td&gt;
&lt;td&gt;1,033&lt;/td&gt;
&lt;td&gt;module dependency edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;instantiates&lt;/td&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;new X()&lt;/code&gt; sites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extends&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;class/interface inheritance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;implements&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;interface implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now look at what is &lt;strong&gt;not&lt;/strong&gt; there. No "type" nodes. No generic-instantiation edges. No data-flow edges. No "this dynamic dispatch resolves to that concrete method" edges. CodeGraph extracts &lt;code&gt;calls&lt;/code&gt;, &lt;code&gt;references&lt;/code&gt;, &lt;code&gt;extends&lt;/code&gt;, &lt;code&gt;implements&lt;/code&gt; — relationships that are &lt;em&gt;locally apparent in the syntax&lt;/em&gt; — and stops. The first-order reading of this is "because tree-sitter doesn't resolve types." True, but circular. The deeper reading is &lt;strong&gt;why this division of labor is correct for an LLM consumer.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The information-theoretic case
&lt;/h3&gt;

&lt;p&gt;A type-checker (or full LSP) does work the LLM cannot easily redo: resolving &lt;code&gt;obj.method()&lt;/code&gt; to the actual method given the static type of &lt;code&gt;obj&lt;/code&gt;, propagating types through generics, walking an inheritance chain to the method actually invoked. That requires the full compilation context — every transitive import, every type definition, every generic instantiation. The cost is high (a build environment, slow cold start, breaks when the build breaks) and the benefit is narrow: precise semantic resolution that's genuinely hard to reconstruct from local context.&lt;/p&gt;

&lt;p&gt;A syntactic extractor does &lt;em&gt;different&lt;/em&gt; work. It makes the structure of the source queryable, but only the structure that's locally apparent: "function &lt;code&gt;dispatch&lt;/code&gt; defined at &lt;code&gt;hono-base.ts:406&lt;/code&gt;, calls &lt;code&gt;match&lt;/code&gt; here, imported from &lt;code&gt;router&lt;/code&gt;." No types, no generics, no runtime binding — but no compilation either.&lt;/p&gt;

&lt;p&gt;The information-theoretic question is: &lt;strong&gt;given an LLM that's good at semantic reasoning but bad at structural enumeration, what's the right split between what the index provides and what the LLM provides?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CodeGraph's answer: hand the LLM the &lt;em&gt;structural skeleton&lt;/em&gt; — what calls what, what's defined where, what imports what — because enumerating that across thousands of files is exactly the part the LLM is bad at and would burn dozens of tool calls trying to do by hand. Leave the &lt;em&gt;semantic resolution&lt;/em&gt; — what does this call actually invoke at runtime under dynamic dispatch? — to the LLM, because the LLM is reasonable at that once the relevant code is in its context, and baking a type resolver into the index would multiply the build cost for a recovery the LLM mostly doesn't need.&lt;/p&gt;

&lt;p&gt;The clean way to see this boundary is the &lt;code&gt;contains&lt;/code&gt; + &lt;code&gt;calls&lt;/code&gt; + &lt;code&gt;references&lt;/code&gt; edges (7,059 of the 8,225) versus the things that &lt;em&gt;aren't&lt;/em&gt; edges at all. When the companion benchmark's Q1 asked how a &lt;code&gt;GET /users/:id&lt;/code&gt; request reaches its handler, what CodeGraph gave Claude Code was the call chain — &lt;code&gt;fetch&lt;/code&gt; → &lt;code&gt;dispatch&lt;/code&gt; → &lt;code&gt;match&lt;/code&gt; — as graph edges. What it did &lt;em&gt;not&lt;/em&gt; give, and didn't try to, was which concrete &lt;code&gt;match&lt;/code&gt; implementation runs given Hono's &lt;code&gt;SmartRouter&lt;/code&gt; picking &lt;code&gt;RegExpRouter&lt;/code&gt; at runtime. The graph located the players; the LLM read the three files and resolved the dispatch. That's the split working as designed: enumeration from the index, resolution from the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  The boundary is a literal table
&lt;/h3&gt;

&lt;p&gt;Here's the detail that turns this from an argument into an observation. When tree-sitter sees a reference it cannot statically resolve to a definition, CodeGraph does not invent an edge. It writes a row to a separate &lt;code&gt;unresolved_refs&lt;/code&gt; table — name, location, the node it came from, no target. The schema has a first-class place for &lt;em&gt;"I saw a use here, I could not prove what it binds to."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On Hono, &lt;code&gt;unresolved_refs&lt;/code&gt; has zero rows — and, as it turns out, so did every other repo I indexed to check it (Section 3 has that result, and it's not the one I expected). The empty table isn't the interesting part; the table &lt;em&gt;existing&lt;/em&gt; is the architecture stating its own boundary. A tool that faked those edges — guessed a target to make the graph look complete — would be lying to the LLM in exactly the way that produces confident wrong answers. CodeGraph's choice to record the unresolved reference &lt;em&gt;as unresolved&lt;/em&gt; is the same discipline a good cache has when it marks an entry stale instead of serving it: the honest move is to represent "don't know," not to paper over it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters beyond CodeGraph
&lt;/h3&gt;

&lt;p&gt;This boundary — &lt;em&gt;syntactic graph for the index, semantic reasoning for the LLM&lt;/em&gt; — is the line the next generation of LLM-coding tools will either hold or violate. The violations are predictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Too far toward semantics in the index&lt;/strong&gt;: a tool that tries to be a full LSP-plus for the LLM. High build cost, slow cold start, fragile on broken builds, marginal benefit because the LLM can do that resolution from local context anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too far toward raw text in the index&lt;/strong&gt;: a tool that's just "grep with nicer indexing" — fast and broad, but it doesn't hand the LLM the structural skeleton it actually needs. That's the position grep+loop already occupies; an index there adds little.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CodeGraph sits in the middle, and that position is right for current LLM capability. As models get better at semantic resolution the line will move one way; as tool-loop iteration gets cheaper it will move the other. But the &lt;em&gt;principle&lt;/em&gt; — that there's an information-theoretic boundary worth picking, and that picking it requires modeling the LLM's real strengths and weaknesses — is the durable take. The right way to evaluate any new LLM-retrieval tool starts here: what does it choose to extract, what does it leave for the LLM, and is that split calibrated for what an LLM is actually good at?&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 2 — SQLite + FTS5 vs vector DB: the cost curve, recursed
&lt;/h2&gt;

&lt;p&gt;CodeGraph stores its symbol graph in a local SQLite database. Not Chroma. Not Pinecone. Not Weaviate. Not Qdrant. The full table list from Hono's index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nodes              edges              files
unresolved_refs    nodes_fts          schema_versions
project_metadata   (+ FTS5 shadow tables: nodes_fts_data/idx/docsize/config)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nodes&lt;/code&gt; and &lt;code&gt;edges&lt;/code&gt; are plain relational tables. &lt;code&gt;nodes_fts&lt;/code&gt; is an FTS5 virtual table. Searching the whole schema for an embedding column, a vector type, a float array — anything ANN-shaped — returns nothing. The only &lt;code&gt;BLOB&lt;/code&gt; columns are FTS5's own internal segment storage (&lt;code&gt;nodes_fts_data&lt;/code&gt;), not vectors. &lt;strong&gt;There are no embeddings in CodeGraph.&lt;/strong&gt; That's not an omission; it's the architecture, and it's the same call the prior post made one level down.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost-curve frame, recursed
&lt;/h3&gt;

&lt;p&gt;The prior post argued vector RAG over a codebase pays a build cost (chunk + embed every file), a maintain cost (re-embed on change, reconcile cross-chunk references), and a low per-query cost (ANN search + rerank) — and that for most repos this loses to grep+loop's &lt;em&gt;(zero build, zero maintain, per-query round-trips)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Apply that exact frame to CodeGraph's own storage. If CodeGraph used a vector DB for its symbols, it would pay: embed every symbol's signature and body on index; re-embed on every file save (the file-watcher would have to fire embedding calls); ANN search per query. That's the same curve the prior post argued &lt;em&gt;against&lt;/em&gt; — and CodeGraph's &lt;em&gt;workload&lt;/em&gt; doesn't justify it, because the queries it serves are &lt;strong&gt;exact lookups&lt;/strong&gt;, not similarity searches. The schema proves the queries are exact by the indexes it builds for them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Find symbol &lt;code&gt;getUserById&lt;/code&gt;"&lt;/strong&gt; → &lt;code&gt;idx_nodes_name&lt;/code&gt;, and &lt;code&gt;idx_nodes_lower_name&lt;/code&gt; for case-insensitive matches. A B-tree probe, microseconds. FTS5 (&lt;code&gt;nodes_fts&lt;/code&gt; over &lt;code&gt;name, qualified_name, docstring, signature&lt;/code&gt;) handles the fuzzier "name contains" variants. No similarity math.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Who calls &lt;code&gt;Context.set&lt;/code&gt;?"&lt;/strong&gt; → &lt;code&gt;idx_edges_target_kind&lt;/code&gt; (a reverse-edge index on &lt;code&gt;(target, kind)&lt;/code&gt;). Reverse adjacency lookup, deterministic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"What does &lt;code&gt;dispatch&lt;/code&gt; call?"&lt;/strong&gt; → &lt;code&gt;idx_edges_source_kind&lt;/code&gt; (the forward-edge index). Forward adjacency, deterministic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Trace &lt;code&gt;fetch&lt;/code&gt; → &lt;code&gt;db_query&lt;/code&gt;"&lt;/strong&gt; → repeated forward-edge hops over those same indexed edges. Graph traversal on stored adjacency, no vectors anywhere in the loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those forward and reverse edge indexes are the whole ballgame. Callers and callees — the queries a code-intelligence tool exists to answer — are a single indexed adjacency lookup in each direction. Vector search cannot do this &lt;em&gt;better&lt;/em&gt;; it can only do it &lt;em&gt;fuzzier and more expensively&lt;/em&gt;, because "who calls this function" has an exact answer that an approximate-nearest-neighbor index would blur.&lt;/p&gt;

&lt;p&gt;The only queries where vector search genuinely helps are semantic ones with no symbol to anchor on — &lt;em&gt;"show me the code that does authentication."&lt;/em&gt; CodeGraph doesn't serve those. The LLM does, by issuing a sequence of exact structural queries and reasoning across the results. The division is the same one from Section 1: the index answers the exact-lookup questions deterministically; the LLM answers the fuzzy-intent questions by orchestrating exact lookups. Neither needs an embedding.&lt;/p&gt;

&lt;h3&gt;
  
  
  The recursion as a design principle
&lt;/h3&gt;

&lt;p&gt;What's elegant — and worth surfacing for its own sake — is that &lt;strong&gt;CodeGraph's storage choice is consistent with the retrieval philosophy from the prior post, one level up.&lt;/strong&gt; Both arguments are the same sentence: &lt;em&gt;exact-lookup workloads should use exact-lookup tools; approximation overhead is paid only where approximation pays back.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If CodeGraph had reached for Chroma over FTS5, it would have violated its own retrieval philosophy — paying embedding and ANN cost to answer questions that have exact answers. That it didn't, that the designer recognized the symbol-graph workload is exact-lookup-shaped and picked the cheapest exact-lookup storage available, is what makes the architecture coherent across layers rather than just locally clever.&lt;/p&gt;

&lt;p&gt;The next tool in this class will face the same fork, and most will reach for a vector DB by default, because "AI tooling = vector store" is the reflex. CodeGraph's choice is the corrective: ask what your &lt;em&gt;workload&lt;/em&gt; needs, not what the category's fashion suggests. That's the cost-curve frame functioning as a meta-design tool — every time you add a layer to an LLM stack, ask which side of the curve the new layer's workload sits on, and pick storage and algorithm from the answer, not the trend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Section 3 — Where CodeGraph's abstraction leaks
&lt;/h2&gt;

&lt;p&gt;Every index lies a little. The question is &lt;em&gt;where&lt;/em&gt; it lies and &lt;em&gt;whether you can tell when it does.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;CodeGraph's graph is built from syntactic extraction, so &lt;strong&gt;anywhere the runtime semantics diverge from the syntactic structure, the graph is incomplete in a way that's hard to detect from the index alone.&lt;/strong&gt; The leak isn't a bug; it's the abstraction working as designed, at a layer that structurally cannot see certain phenomena. There's a tell for it in the schema, and there's a part the schema can't tell you about — and the difference between those two is the whole point.&lt;/p&gt;

&lt;h3&gt;
  
  
  The honest part: the provenance column
&lt;/h3&gt;

&lt;p&gt;CodeGraph stamps every edge with a &lt;code&gt;provenance&lt;/code&gt; value. On Hono, 8,218 of the 8,225 edges have empty provenance — meaning &lt;em&gt;direct from the syntax tree&lt;/em&gt; — and exactly &lt;strong&gt;7&lt;/strong&gt; carry the value &lt;code&gt;heuristic&lt;/code&gt;. Those seven are edges CodeGraph's framework adapters &lt;em&gt;inferred&lt;/em&gt; from a recognized pattern rather than read off the AST: route registrations, framework binding conventions, the handful of cases where a tool that "supports Hono / Flask / Spring" pattern-matches a known idiom and synthesizes an edge the raw syntax doesn't spell out.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;heuristic&lt;/code&gt; tag is the architecture being honest. It is, in the vocabulary of the memory post in this series, an &lt;em&gt;arrow&lt;/em&gt;: every edge points back to &lt;em&gt;how it was derived&lt;/em&gt;, and the seven guessed edges are flagged as guesses. A consumer that cared could treat heuristic edges with less trust than syntactic ones. That's good cache hygiene — the index records the confidence of its own entries instead of presenting all of them as equally certain.&lt;/p&gt;

&lt;h3&gt;
  
  
  The part the schema can't tell you about
&lt;/h3&gt;

&lt;p&gt;Here's the catch, and it's the one that matters: &lt;strong&gt;the provenance column only flags edges that exist.&lt;/strong&gt; The dangerous leak isn't a guessed edge that's marked as guessed. It's the edge that &lt;em&gt;should&lt;/em&gt; exist and isn't there at all — because the relationship lives in a layer tree-sitter cannot see, so there's nothing to extract, nothing to tag, and nothing to warn you. The four big zones where this happens:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Macro-heavy code.&lt;/strong&gt; In Rust, &lt;code&gt;vec![1, 2, 3]&lt;/code&gt; expands at compile time into a call sequence the AST never contains; the graph shows a &lt;code&gt;vec!&lt;/code&gt; invocation, not the &lt;code&gt;Vec::new()&lt;/code&gt; + &lt;code&gt;push()&lt;/code&gt; that actually runs. For procedural macros (&lt;code&gt;#[derive(...)]&lt;/code&gt;, attribute macros), the &lt;em&gt;generated implementation&lt;/em&gt; is what executes and CodeGraph can't see into it without running the compiler — which would forfeit the no-compile property that Section 1 showed is the whole point. Same shape in C/C++ preprocessor-heavy code, Lisp/Clojure macros, Elixir compile-time metaprogramming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metaprogramming.&lt;/strong&gt; Python decorators routinely rewrite functions: &lt;code&gt;@dataclass&lt;/code&gt; synthesizes &lt;code&gt;__init__&lt;/code&gt;/&lt;code&gt;__repr__&lt;/code&gt;/&lt;code&gt;__eq__&lt;/code&gt;; &lt;code&gt;@app.route("/users")&lt;/code&gt; registers a handler with a router. Tree-sitter sees the decorator and the function as adjacent syntax, not the synthesis or the registration. CodeGraph's framework adapters catch the &lt;em&gt;common&lt;/em&gt; cases — and that's literally what the 7 heuristic edges on Hono are — but arbitrary user-defined decorators that mutate behavior are invisible. Ruby &lt;code&gt;method_missing&lt;/code&gt;, Python &lt;code&gt;__getattr__&lt;/code&gt;, Java reflection: same story. The graph confidently returns "no callers" for a method invoked entirely through reflection, and the LLM, trusting structured output, may hand you a confidently wrong blast radius.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated code.&lt;/strong&gt; Protobuf, GraphQL codegen, OpenAPI clients, ORM model generation (Prisma, SQLAlchemy declarative), JSX/Svelte compilation — the code the runtime executes isn't the code in source control. It lives in &lt;code&gt;build/&lt;/code&gt;, &lt;code&gt;dist/&lt;/code&gt;, &lt;code&gt;.cache/&lt;/code&gt;, places &lt;code&gt;.gitignore&lt;/code&gt; excludes. CodeGraph indexes what's checked in; the generated layer is outside the boundary. "Who implements &lt;code&gt;UserService&lt;/code&gt;?" returns the hand-written interface, not the generated stub that implements it on the wire. Any source-only index has this; it's worth naming because it interacts badly with the user's instinct that &lt;em&gt;an "AST graph" must be complete.&lt;/em&gt; It's complete over the source it indexed — and the generated layer was never in that source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JIT and runtime-registered bindings.&lt;/strong&gt; DI containers (Spring, Guice, Dagger, ASP.NET service collection), &lt;code&gt;FastAPI Depends&lt;/code&gt;, plugin systems with runtime registration, and — the one the companion benchmark hit directly — &lt;strong&gt;middleware chains composed at app startup.&lt;/strong&gt; Hono's &lt;code&gt;app.use(...)&lt;/code&gt; builds the middleware array at runtime; tree-sitter sees the &lt;code&gt;use&lt;/code&gt; call sites and the handler as unconnected syntax. When the benchmark's Q2 asked Claude Code to trace the middleware call stack, what &lt;code&gt;codegraph_trace&lt;/code&gt; could return was the &lt;em&gt;syntactic&lt;/em&gt; call chain through &lt;code&gt;compose()&lt;/code&gt; — accurate as far as it goes, and genuinely fewer steps than baseline grep — but the actual runtime ordering of middlewares is assembled by &lt;code&gt;app.use&lt;/code&gt; calls scattered across the app, which the graph doesn't compose. The trace looked authoritative and was structurally real; it just wasn't the runtime composition, and only someone who knew the leak zone would know to check.&lt;/p&gt;

&lt;h3&gt;
  
  
  The empirical check, and the null result that sharpens it
&lt;/h3&gt;

&lt;p&gt;I expected &lt;code&gt;unresolved_refs&lt;/code&gt; to be where this shows up — index a macro-heavy repo, watch the table fill. So I indexed three to test it: Hono (TypeScript), &lt;a href="https://github.com/pallets/click" rel="noopener noreferrer"&gt;click&lt;/a&gt; (Python, decorator-heavy), and &lt;a href="https://github.com/ron-rs/ron" rel="noopener noreferrer"&gt;ron&lt;/a&gt; (a Rust crate leaning on &lt;code&gt;derive&lt;/code&gt; macros and serde). &lt;code&gt;unresolved_refs&lt;/code&gt; was &lt;strong&gt;zero on all three&lt;/strong&gt;; heuristic edges were 7, 0, and 0. The null result &lt;em&gt;is&lt;/em&gt; the finding. A &lt;code&gt;#[derive(Serialize)]&lt;/code&gt; impl never appears as an unresolved reference, because nothing in the source ever wrote a reference to it to leave dangling — the impl only exists after macro expansion. &lt;code&gt;codegraph callers serialize&lt;/code&gt; on ron returns its seven real syntactic callers and silently omits whatever the derive generates, with no flag and no empty-table warning, because from the index's point of view nothing is missing. And that is the trap. &lt;strong&gt;An empty &lt;code&gt;unresolved_refs&lt;/code&gt; table reads like a clean bill of health, but on derive-heavy or reflection-heavy code it means the opposite of "everything resolved" — it means the thing that didn't resolve never left a trace to flag.&lt;/strong&gt; The table catches references it can't resolve; it cannot catch code that was never written down to reference. That's the leak that costs you: not the guess that gets flagged, but the absence that looks exactly like completeness. It's the same failure shape as the memory post's &lt;em&gt;"could" stored as "did"&lt;/em&gt; — the dangerous error is always the one that wears the face of a correct answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why mapping the leaks matters
&lt;/h3&gt;

&lt;p&gt;A tool you trust everywhere is a tool you stop checking. &lt;strong&gt;The four zones above are where the LLM, trusting the graph, gives you confidently wrong answers&lt;/strong&gt; — and those are the failures that cost real engineering time, because the answer &lt;em&gt;looks right&lt;/em&gt; and you have no reason to second-guess it.&lt;/p&gt;

&lt;p&gt;The practical rule is small. Inside one of these zones — heavy macros, reflection/DI, codegen-heavy projects, runtime-composed bindings — CodeGraph is still a fine &lt;em&gt;starting point&lt;/em&gt;, but the LLM's answer has to be cross-checked against the runtime, not against the graph. Outside them — most application code in most languages, which is most of what most people query — the graph is enough. The provenance column tells you which &lt;em&gt;present&lt;/em&gt; edges were guessed; nothing tells you which &lt;em&gt;absent&lt;/em&gt; edges were never seen. That asymmetry is the actual trust boundary, and it's the thing to internalize before you wire any syntactic index into an agent's decision loop. Joel Spolsky named this pattern for compilers and frameworks twenty years ago — every abstraction leaks, and you pay for the leak precisely when you've forgotten the abstraction is there. CodeGraph is the latest data point in a very old series.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mapping CodeGraph to the six conditions
&lt;/h2&gt;

&lt;p&gt;Field-by-field, how CodeGraph hits each condition from &lt;em&gt;Agent Retrieval Is a Cost Curve Problem&lt;/em&gt;. Compressed; the prior post defines the conditions, the companion post applies them empirically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. No-compile parsing.&lt;/strong&gt; Tree-sitter parses source into an AST with no build invocation, no dependency resolution, no language environment. On Hono, 362 files indexed to 4,128 nodes and 8,225 edges in &lt;strong&gt;1.7 seconds&lt;/strong&gt;; the published 7-repo benchmark reports first-index on the order of minutes for VS Code-scale (~30k files), all subsequent updates incremental. LSP needs &lt;code&gt;tsc&lt;/code&gt; / &lt;code&gt;cargo check&lt;/code&gt; / &lt;code&gt;mvn&lt;/code&gt;; CodeGraph reads raw text. &lt;strong&gt;Met.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Language portability.&lt;/strong&gt; ~19 languages via tree-sitter, plus framework adapters for route-aware extraction (Hono's 873 &lt;code&gt;route&lt;/code&gt; nodes come from one of them). One binary, no per-language server. &lt;strong&gt;Met.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. LLM-shaped API.&lt;/strong&gt; Here the scaffold version of this post — and a lot of the casual coverage — gets a fact wrong worth correcting precisely. The CLI exposes a dozen commands (&lt;code&gt;query&lt;/code&gt;, &lt;code&gt;callers&lt;/code&gt;, &lt;code&gt;callees&lt;/code&gt;, &lt;code&gt;impact&lt;/code&gt;, &lt;code&gt;affected&lt;/code&gt;, &lt;code&gt;context&lt;/code&gt;, …). But the &lt;strong&gt;MCP server exposes exactly five tools&lt;/strong&gt; to the agent: &lt;code&gt;codegraph_search&lt;/code&gt; (locations only), &lt;code&gt;codegraph_context&lt;/code&gt; (described in its own schema as the &lt;em&gt;PRIMARY tool, call FIRST for any how-does-X-work question&lt;/em&gt;), &lt;code&gt;codegraph_node&lt;/code&gt; (one symbol plus its callers/callees trail), &lt;code&gt;codegraph_explore&lt;/code&gt; (several related symbols in one capped call), and &lt;code&gt;codegraph_trace&lt;/code&gt; (the call path between two symbols). The narrowing is the design: the human CLI gets &lt;code&gt;impact&lt;/code&gt; and &lt;code&gt;affected&lt;/code&gt; as separate verbs; the agent gets a &lt;em&gt;context-first&lt;/em&gt; surface of five flat tools, each returning &lt;code&gt;{symbol, file, line, snippet, related[]}&lt;/code&gt;-shaped records, with the instruction snippet steering it to &lt;code&gt;codegraph_context&lt;/code&gt; before anything else. Ten tools would be worse for an LLM than five; CodeGraph picked five. &lt;strong&gt;Met, deliberately.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Coverage breadth.&lt;/strong&gt; Symbol graph for structure; FTS5 over &lt;code&gt;name, qualified_name, docstring, signature&lt;/code&gt; for text-fallback; Claude Code's native Grep stays enabled for everything outside the index. &lt;strong&gt;Partially met — the correct partial.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Live update without reindex.&lt;/strong&gt; OS file-watcher with a short debounce; a save re-parses the touched file and re-resolves dependents' import edges. &lt;strong&gt;Met.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Zero-config install.&lt;/strong&gt; Single binary, one-line install, auto-detects the agent, writes the MCP config and the instruction snippet, then &lt;code&gt;codegraph init -i&lt;/code&gt; builds the index. Ten minutes from curiosity to working under ~1,000 files. &lt;strong&gt;Met.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Six for six. The architecture the prior post argued was theoretically right but practically missing exists, in production, with a working installer — and, read against its own schema, the choices hold up under inspection rather than just on the landing page.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this says about LLM retrieval as a discipline
&lt;/h2&gt;

&lt;p&gt;Three things, in increasing order of generality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The right LLM-index design is not a copy of human-IDE design.&lt;/strong&gt; Sourcegraph and LSP were built for a human reading one precise answer; an LLM reads many cheap rounds and reasons across them. The architectures should differ, and CodeGraph's choices — tree-sitter not LSP, five flat MCP tools not a nested LSP API, FTS5 not vectors — are evidence of someone designing for the actual consumer instead of porting an existing design. The framework predicts the design space, and the interesting variation between the tools that will fill it is not in the six conditions (those are now the table stakes) but in the &lt;em&gt;ranking&lt;/em&gt; layer — how each one orders the symbols a query surfaces. That's where the next tool will try to win, and where the next benchmark should aim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The cost-curve frame is recursive.&lt;/strong&gt; It applies to every layer of an LLM stack, including the tools that wrap the LLM. CodeGraph's FTS5-not-Chroma choice is the same shape as the original grep-not-RAG choice. Use it as a meta-design tool: at every layer, ask which side of the curve the workload sits on, and let that pick the storage and the algorithm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The abstraction leaks are the trust boundary — and trust, in the end, has to terminate at the source.&lt;/strong&gt; This is the thread that runs through the whole series. CodeGraph's graph is a &lt;em&gt;derived view&lt;/em&gt; of the source: a cache. Its &lt;code&gt;heuristic&lt;/code&gt; provenance tags and its &lt;code&gt;unresolved_refs&lt;/code&gt; table are the parts where it keeps an arrow back to that source and is honest about what it did and didn't see. But a syntactic graph is still a lossy projection of a running program, and the leak zones are exactly where the projection drops information that only exists at runtime. The discipline that falls out of this is the same one the retrieval post and the memory post arrived at from their own directions: &lt;strong&gt;a derived artifact is trustworthy only where you can check it against the source that produced it.&lt;/strong&gt; CodeGraph is fast and exact in the 80% of code where syntax determines structure, and quietly incomplete in the 20% where it doesn't — and the only way to stay out of the failure modes is to remember the graph is a cache and keep the real code, the actual runtime, as the thing that wins every conflict.&lt;/p&gt;

&lt;p&gt;The bigger move CodeGraph represents — &lt;em&gt;third-party MCP tools filling the retrieval gap the foundation model's main agent doesn't fill&lt;/em&gt; — is the ecosystem direction the feature-flag analysis in the prior post suggested Anthropic is hedging toward. Whether Anthropic eventually builds tree-sitter symbol-graph functionality natively or leaves it to the CodeGraph-class ecosystem is a product call. The technical case for "let MCP fill it" is strong: the design space is still settling, and locking one approach into Claude Code spends option value the ecosystem is currently pricing for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing — the mini-series arc
&lt;/h2&gt;

&lt;p&gt;This is the third of a three-part Lab series on Claude Code's retrieval and memory architectures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;&lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;Agent Retrieval Is a Cost Curve Problem&lt;/a&gt;&lt;/em&gt; (2026-05-25) — why grep+loop, not RAG, for most projects&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;&lt;a href="https://harrisonsec.com/blog/agent-memory-is-a-cache-coherence-problem/" rel="noopener noreferrer"&gt;Agent Memory Is a Cache Coherence Problem&lt;/a&gt;&lt;/em&gt; (2026-05-28) — why hand-curated Markdown, not lossy vector recall, for cross-session memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This post&lt;/strong&gt; (2026-06-08) — what lives above the cost-curve crossover: CodeGraph as the architecturally coherent symbol-graph companion the first post argued was missing, read first-principles against its own index for what its choices say about the discipline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read together, the three describe one stance on agent retrieval and memory: choose &lt;strong&gt;lossless and exact&lt;/strong&gt; by default; expose &lt;strong&gt;MCP&lt;/strong&gt; as the integration substrate; let third-party tools fill the gaps you don't want to own; and keep an arrow back to the source everywhere, because every derived view is a cache and the source is the only thing that can't drift from itself. The cost-curve frame is the math, the cache-coherence frame is the failure taxonomy, and the first-principles read of CodeGraph is what the architecture, looked at carefully, says about where LLM retrieval is going.&lt;/p&gt;

&lt;p&gt;If you're building agent retrieval, the three frames are now in your toolkit. The companion empirical post gives you the install-or-not decision; this one gives you the lens for the next ten tools that ship in the same space.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Companion piece 1 (this is the third in a 3-post Lab series): *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;Agent Retrieval Is a Cost Curve Problem: Why Claude Code Doesn't Use RAG&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;Companion piece 2: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/agent-memory-is-a-cache-coherence-problem/" rel="noopener noreferrer"&gt;Agent Memory Is a Cache Coherence Problem&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;Empirical pair on the Operator track: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/i-tested-codegraph-on-hono-benchmark/" rel="noopener noreferrer"&gt;I Tested CodeGraph on Hono. The Tool-Call Savings Reproduce — the Cost Savings Don't.&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;Background: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/consistency-scenarios-and-approaches-production/" rel="noopener noreferrer"&gt;Consistency in Distributed Systems: Scenarios, Trade-offs, and What Actually Works&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;CodeGraph repo: *&lt;/em&gt;&lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;https://github.com/colbymchenry/codegraph&lt;/a&gt;***&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>sqlite</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Tested CodeGraph on Hono. The Tool-Call Savings Reproduce — the Cost Savings Don't.</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Mon, 01 Jun 2026 22:39:59 +0000</pubDate>
      <link>https://dev.to/harrisonsec/i-tested-codegraph-on-hono-the-tool-call-savings-reproduce-the-cost-savings-dont-389p</link>
      <guid>https://dev.to/harrisonsec/i-tested-codegraph-on-hono-the-tool-call-savings-reproduce-the-cost-savings-dont-389p</guid>
      <description>&lt;p&gt;Two weeks ago CodeGraph hit GitHub trending — tree-sitter + SQLite/FTS5 + MCP for Claude Code, 19k+ stars in a week. The team published a benchmark on 7 repos showing &lt;strong&gt;35% cheaper, 57% fewer tokens, 46% faster, 71% fewer tool calls&lt;/strong&gt; vs. baseline.&lt;/p&gt;

&lt;p&gt;Those are big numbers. They're also numbers from a benchmark designed by the team that built the tool, on repos they chose. Designer bias is the #1 risk in any retrieval benchmark — when you pick the test repos and write the ground truth, you'll consciously or unconsciously favor your own tool's strengths.&lt;/p&gt;

&lt;p&gt;So I ran an independent test on an 8th repo — &lt;strong&gt;Hono&lt;/strong&gt; (TypeScript, ~280 source files, in neither CodeGraph's published 7-repo suite nor any other published benchmark I could find). 5 architectural questions covering different retrieval shapes, with a deliberate control case (Q5) where the tool should not win. Two conditions (baseline grep+Read+Glob+Explore vs. CodeGraph active), &lt;strong&gt;4 repeats&lt;/strong&gt; per question per condition. 40 runs on Claude Opus 4.8 — and, critically, &lt;strong&gt;every CodeGraph run was verified to have connected, and actual &lt;code&gt;codegraph_*&lt;/code&gt; tool usage was recorded per run&lt;/strong&gt; (more on why that sentence exists below).&lt;/p&gt;

&lt;p&gt;The result splits in a way the single published headline number hides — and the split is the useful part.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — On Hono, CodeGraph delivers a &lt;strong&gt;large, consistent reduction in tool calls (-55%, 14.0 → 6.3 avg) and a smaller latency win (-20%)&lt;/strong&gt; — the published 7-repo direction reproduces here. But &lt;strong&gt;cost is a wash: +6.8%&lt;/strong&gt;, not the published −35%. On narrow-scope questions (route lookup, middleware trace) CodeGraph is actually &lt;strong&gt;20-43% more expensive&lt;/strong&gt;, because each structural lookup loads a big chunk of graph context that costs more in cached tokens than the grep round-trips it replaces. The cost win only appears on broad multi-file navigation (Q3 multi-runtime adapters: &lt;strong&gt;−29% cost, −80% tool calls, −53% latency&lt;/strong&gt;). A second finding: baseline grep+Read has &lt;strong&gt;high variance&lt;/strong&gt; — the agent occasionally spiraled to 47-52 tool calls on the broad questions, while CodeGraph never exceeded 16. &lt;strong&gt;Net at Hono's size: CodeGraph makes the agent take fewer steps and finish faster, but not for fewer dollars.&lt;/strong&gt; Total cost of the 40 valid runs: ~$14 of Opus 4.8 calls. Raw per-run CSV and the 5 verbatim prompts are below.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What "tool calls down, cost flat" actually means
&lt;/h2&gt;

&lt;p&gt;CodeGraph's published 7-repo suite (VS Code, Excalidraw, Django, Tokio, OkHttp, Gin, Alamofire) skews larger and more architecturally complex than Hono. Hono is ~280 TypeScript source files (362 files indexed by CodeGraph, including tests and configs), 16MB on disk — small enough that a thoughtful agent with grep + Read can finish most architectural questions in a handful of tool calls.&lt;/p&gt;

&lt;p&gt;The interesting result is that the &lt;em&gt;axes come apart&lt;/em&gt;. CodeGraph replaces several grep+Read round-trips with one or two structural lookups — so &lt;strong&gt;step count drops hard (-55%)&lt;/strong&gt;. But each &lt;code&gt;codegraph_context&lt;/code&gt; / &lt;code&gt;codegraph_explore&lt;/code&gt; call returns a sizeable chunk of graph context, which then rides along in the conversation cache and gets re-read every turn. At Hono's size, the dollar cost of carrying that cached payload roughly equals the dollar cost of the grep round-trips it replaced — so &lt;strong&gt;dollars stay flat (+7%) even as steps fall by more than half&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's not a contradiction of the cost-curve thesis from &lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;the prior post in this mini-series&lt;/a&gt; — it's a sharper reading of it. Hono sits &lt;strong&gt;above&lt;/strong&gt; the step-count crossover (the index already saves tool calls) but &lt;strong&gt;below&lt;/strong&gt; the dollar crossover (it doesn't yet save money). On a much bigger repo, the grep path churns through far more files and the index pays back on dollars too. Hono just happens to land in the gap between the two crossovers.&lt;/p&gt;

&lt;p&gt;A useful complementary benchmark answers three things the published one doesn't:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cross-validation on a repo not chosen by the tool's team&lt;/strong&gt; — do the published advantages generalize?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Within-repo variance across question types&lt;/strong&gt; — does the win concentrate on certain question shapes? (It does — heavily.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A control case where the tool shouldn't win&lt;/strong&gt; — Q5 (text search) tests whether the agent correctly &lt;em&gt;declines&lt;/em&gt; to use the structural engine when grep is the right tool.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setup — install CodeGraph, ~10 minutes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# install (downloads a single binary, no Node/npm required)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh

&lt;span class="c"&gt;# clone the test repo + index it&lt;/span&gt;
git clone https://github.com/honojs/hono.git ~/tmp/hono
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/tmp/hono
codegraph init &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Index build time on Hono (362 files, 4,128 nodes, 8,225 edges): &lt;strong&gt;1.7 seconds.&lt;/strong&gt; On-disk index: 7.1 MB.&lt;/p&gt;

&lt;p&gt;Per-condition setup for the two arms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline (control):&lt;/strong&gt; a clean copy of Hono via &lt;code&gt;rsync -a --exclude='.codegraph/'&lt;/code&gt; to a separate directory so Claude couldn't accidentally grep into the index. No MCP servers registered. Agent uses native Glob + Grep + Read + Explore + Task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CodeGraph active:&lt;/strong&gt; original Hono directory with &lt;code&gt;.codegraph/&lt;/code&gt; present, MCP server registered:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"codegraph"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"codegraph"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"serve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--mcp"&lt;/span&gt;&lt;span class="p"&gt;]}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both arms run &lt;code&gt;claude --print --output-format stream-json --model opus&lt;/code&gt; so the model and the rest of the agent loop are identical; the only varying input is whether the CodeGraph MCP server is in the loop. Each run is a fresh session with no prior context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying the tool actually ran (this is not optional)
&lt;/h3&gt;

&lt;p&gt;A retrieval-tool benchmark is only valid if the tool is actually in the loop — and I learned that the hard way. My first pass at this benchmark silently ran with CodeGraph's MCP server &lt;strong&gt;never connected&lt;/strong&gt;: the config was missing the &lt;code&gt;--mcp&lt;/code&gt; flag, and Claude Code proceeds without a server that fails its hand-shake in time rather than erroring out. Every "CodeGraph" run was really just grep+Read. The comparison was noise, and the numbers looked plausibly small — which is exactly how a broken benchmark slips through.&lt;/p&gt;

&lt;p&gt;So for the data here, every run is instrumented:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--strict-mcp-config&lt;/code&gt;&lt;/strong&gt; — only the server under test is loaded, with no contamination from other globally-registered MCP servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-warmed daemon + &lt;code&gt;MCP_TIMEOUT=30000&lt;/code&gt;&lt;/strong&gt; — CodeGraph's stdio server attaches to a warm daemon and finishes its handshake &lt;em&gt;before&lt;/em&gt; the agent loop starts. (MCP connection is async; on a fast question the agent can otherwise finish before a cold server is ready.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A per-run assertion&lt;/strong&gt; that CodeGraph reached &lt;code&gt;connected&lt;/code&gt; status, plus a record of whether the agent actually invoked a &lt;code&gt;codegraph_*&lt;/code&gt; tool. Runs that didn't connect were discarded.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All 20 CodeGraph runs in this post connected. The agent invoked CodeGraph on Q1-Q4 (4/4 repeats each) and — correctly — chose grep on the Q5 control (0/4). Most vendor benchmarks never report this check. After watching mine fail it silently, I won't publish a retrieval benchmark without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 questions
&lt;/h2&gt;

&lt;p&gt;Full verbatim prompts in the Appendix. Brief overview:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What it tests&lt;/th&gt;
&lt;th&gt;Hypothesis for CodeGraph&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Route resolution: &lt;code&gt;GET /users/:id&lt;/code&gt; → handler&lt;/td&gt;
&lt;td&gt;Route-aware extraction&lt;/td&gt;
&lt;td&gt;Strong win&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Middleware chain trace through &lt;code&gt;app.use&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Dynamic dispatch tracing&lt;/td&gt;
&lt;td&gt;Decisive win via structural lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-runtime adapter architecture&lt;/td&gt;
&lt;td&gt;Cross-file abstraction&lt;/td&gt;
&lt;td&gt;Mid-strong win&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Refactor impact: add mandatory &lt;code&gt;requestId&lt;/code&gt; to &lt;code&gt;Context&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Impact propagation + completeness&lt;/td&gt;
&lt;td&gt;Strong win (what &lt;code&gt;codegraph_impact&lt;/code&gt; is built for)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text search: every literal &lt;code&gt;'Content-Type'&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Keyword search baseline&lt;/td&gt;
&lt;td&gt;~Parity; agent should &lt;em&gt;decline&lt;/em&gt; the tool&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Q5 is the &lt;strong&gt;CONTROL&lt;/strong&gt; — the tool should not win here, and whether the agent even reaches for it is itself a signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;Each row averaged over 4 repeats. Cost is Claude's own &lt;code&gt;total_cost_usd&lt;/code&gt; (the API's authoritative figure, not my own multiplication); wall latency from request to final token; tool calls counted from unique &lt;code&gt;tool_use&lt;/code&gt; blocks in the transcript; tokens are input + output (cache tokens tracked separately in the &lt;a href="https://harrisonsec.com/codegraph-hono-benchmark-data.csv" rel="noopener noreferrer"&gt;CSV&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost / tokens
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Q&lt;/th&gt;
&lt;th&gt;Baseline cost&lt;/th&gt;
&lt;th&gt;CodeGraph cost&lt;/th&gt;
&lt;th&gt;Δ cost&lt;/th&gt;
&lt;th&gt;Baseline tokens&lt;/th&gt;
&lt;th&gt;CodeGraph tokens&lt;/th&gt;
&lt;th&gt;Δ tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1 route&lt;/td&gt;
&lt;td&gt;$0.321&lt;/td&gt;
&lt;td&gt;$0.393&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;+22.5%&lt;/strong&gt; ❌&lt;/td&gt;
&lt;td&gt;10,115&lt;/td&gt;
&lt;td&gt;6,045&lt;/td&gt;
&lt;td&gt;−40.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2 middleware&lt;/td&gt;
&lt;td&gt;$0.212&lt;/td&gt;
&lt;td&gt;$0.303&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;+43.4%&lt;/strong&gt; ❌&lt;/td&gt;
&lt;td&gt;7,233&lt;/td&gt;
&lt;td&gt;5,649&lt;/td&gt;
&lt;td&gt;−21.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3 multi-runtime&lt;/td&gt;
&lt;td&gt;$0.490&lt;/td&gt;
&lt;td&gt;$0.348&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;−28.9%&lt;/strong&gt; ✓✓&lt;/td&gt;
&lt;td&gt;11,582&lt;/td&gt;
&lt;td&gt;7,048&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−39.1%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4 refactor&lt;/td&gt;
&lt;td&gt;$0.402&lt;/td&gt;
&lt;td&gt;$0.509&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;+26.5%&lt;/strong&gt; ❌&lt;/td&gt;
&lt;td&gt;9,119&lt;/td&gt;
&lt;td&gt;8,567&lt;/td&gt;
&lt;td&gt;−6.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5 text (ctrl)&lt;/td&gt;
&lt;td&gt;$0.267&lt;/td&gt;
&lt;td&gt;$0.253&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−5.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8,874&lt;/td&gt;
&lt;td&gt;8,998&lt;/td&gt;
&lt;td&gt;+1.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aggregate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.338&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.361&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+6.8%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9,385&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7,261&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−22.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Tool calls / wall latency
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Q&lt;/th&gt;
&lt;th&gt;Baseline calls&lt;/th&gt;
&lt;th&gt;CodeGraph calls&lt;/th&gt;
&lt;th&gt;Δ calls&lt;/th&gt;
&lt;th&gt;Baseline latency&lt;/th&gt;
&lt;th&gt;CodeGraph latency&lt;/th&gt;
&lt;th&gt;Δ latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1 route&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;6.8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−12.9%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;49.8s&lt;/td&gt;
&lt;td&gt;51.2s&lt;/td&gt;
&lt;td&gt;+2.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2 middleware&lt;/td&gt;
&lt;td&gt;5.0&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−20.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;41.2s&lt;/td&gt;
&lt;td&gt;43.1s&lt;/td&gt;
&lt;td&gt;+4.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3 multi-runtime&lt;/td&gt;
&lt;td&gt;35.2&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;−80.1%&lt;/strong&gt; ✓✓&lt;/td&gt;
&lt;td&gt;123.7s&lt;/td&gt;
&lt;td&gt;58.4s&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;−52.8%&lt;/strong&gt; ✓✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4 refactor&lt;/td&gt;
&lt;td&gt;19.8&lt;/td&gt;
&lt;td&gt;11.8&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;−40.5%&lt;/strong&gt; ✓&lt;/td&gt;
&lt;td&gt;87.9s&lt;/td&gt;
&lt;td&gt;85.9s&lt;/td&gt;
&lt;td&gt;−2.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q5 text (ctrl)&lt;/td&gt;
&lt;td&gt;2.2&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;−11.1%&lt;/td&gt;
&lt;td&gt;51.2s&lt;/td&gt;
&lt;td&gt;43.5s&lt;/td&gt;
&lt;td&gt;−15.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aggregate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−55.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;70.8s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56.4s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−20.3%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two headline rows: &lt;strong&gt;−55% tool calls&lt;/strong&gt; (real and consistent — CodeGraph used fewer tools on every single question) and &lt;strong&gt;+6.8% cost&lt;/strong&gt; (CodeGraph is &lt;em&gt;not&lt;/em&gt; cheaper on Hono). The latency win (−20%) is real but concentrated: almost all of it is Q3; on Q1/Q2/Q4 latency is within ±5%.&lt;/p&gt;

&lt;h3&gt;
  
  
  The variance story — CodeGraph bounds the worst case
&lt;/h3&gt;

&lt;p&gt;Averages hide the most interesting result. Baseline tool-call counts, per repeat:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Q&lt;/th&gt;
&lt;th&gt;Baseline (4 repeats)&lt;/th&gt;
&lt;th&gt;CodeGraph (4 repeats)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q3 multi-runtime&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14, 23, 52, 52&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5, 6, 8, 9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4 refactor&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9, 10, 13, 47&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9, 10, 12, 16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the broad questions, baseline grep+Read &lt;strong&gt;occasionally spiraled&lt;/strong&gt; — the agent without an index wandered to 47-52 tool calls chasing files. Across all 40 runs, baseline ranged from 2 to &lt;strong&gt;52&lt;/strong&gt; tool calls; CodeGraph ranged from 1 to &lt;strong&gt;16&lt;/strong&gt;. &lt;strong&gt;A large part of CodeGraph's value here isn't the mean — it's that it bounds the worst case.&lt;/strong&gt; When the structural answer is one graph query away, the agent can't spiral. That's a reliability property, not just an efficiency one, and it doesn't show up in a single average.&lt;/p&gt;

&lt;p&gt;A caveat on sample size: this is 4 repeats on one repo. Treat the &lt;strong&gt;magnitudes as indicative and the directions as robust&lt;/strong&gt; — CodeGraph used fewer tool calls in every question and nearly every repeat, and the cost direction was consistent within cells (more expensive on Q1/Q2/Q4, cheaper only on Q3). What I would not over-read is the exact percentages; a 47-vs-9 baseline spread on Q4 means the per-question means carry real uncertainty even at n=4.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-question narrative
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q1 (route resolution) — CodeGraph used, but more expensive.&lt;/strong&gt; Both arms traced &lt;code&gt;app.fetch&lt;/code&gt; → &lt;code&gt;#dispatch&lt;/code&gt; → &lt;code&gt;this.router.match()&lt;/code&gt; and read &lt;code&gt;SmartRouter&lt;/code&gt; → &lt;code&gt;RegExpRouter&lt;/code&gt;. CodeGraph used &lt;code&gt;codegraph_context&lt;/code&gt; + &lt;code&gt;codegraph_trace&lt;/code&gt; (2 calls/run) and cut tool calls 13% and tokens 40% — but cost rose 22.5% and latency was flat. The structural context it front-loaded was heavier than the 1-2 grep steps it saved. &lt;strong&gt;Hono's router is small enough (5-6 files for the full picture) that grep finds it directly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q2 (middleware chain trace) — used, 43% more expensive.&lt;/strong&gt; CodeGraph landed the &lt;code&gt;app.use&lt;/code&gt; → middleware array → &lt;code&gt;compose()&lt;/code&gt; chain in 4 tool calls vs baseline's 5, but cost jumped 43%. Same mechanism as Q1, more pronounced: the call-chain context payload dominated a question baseline answered cheaply in 5 small steps. &lt;strong&gt;The clearest example of "fewer steps, more dollars."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q3 (multi-runtime adapter) — the unambiguous win.&lt;/strong&gt; Enumerating 6 adapter directories (Cloudflare Workers / Deno / Bun / Node / AWS Lambda / Vercel Edge) is exactly where one graph query beats many Glob+grep iterations. Baseline averaged &lt;strong&gt;35 tool calls and 124s&lt;/strong&gt; (and spiraled to 52 twice); CodeGraph: &lt;strong&gt;7 calls, 58s, −29% cost.&lt;/strong&gt; This is the question shape where structural retrieval pays back on every axis at once — and the only one where it saved money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q4 (refactor impact: add &lt;code&gt;requestId&lt;/code&gt; to &lt;code&gt;Context&lt;/code&gt;) — tools halved, cost up.&lt;/strong&gt; Supposed to be CodeGraph's strongest case (&lt;code&gt;codegraph_impact&lt;/code&gt; is built for blast-radius). It did cut tool calls 40% (and tamed baseline's 47-call spiral), but cost rose 26.5%: the impact-graph walk pulled wide context the agent didn't fully need at Hono's size. Completeness was comparable across arms (both identified the &lt;code&gt;Context&lt;/code&gt; constructors in &lt;code&gt;src/hono-base.ts&lt;/code&gt;, the &lt;code&gt;Variables&lt;/code&gt; plumbing, and the per-method handler signatures). &lt;strong&gt;Fewer, more-bounded steps — but the propagation graph isn't wide enough here to pay back on dollars.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q5 (text search, control) — the agent declined the tool, and that's the point.&lt;/strong&gt; On a pure literal-&lt;code&gt;'Content-Type'&lt;/code&gt; search, the agent &lt;strong&gt;never invoked CodeGraph in any of the 4 repeats&lt;/strong&gt; — it reached straight for grep. Result: near-parity (−5% cost, −15% latency, both inside the noise). The old version of this post claimed an "FTS5 fallback" win here; that was an artifact of the broken first run. The truth is simpler and better: &lt;strong&gt;with CodeGraph connected and available, the agent correctly chose grep for a grep-shaped task.&lt;/strong&gt; No over-engineering. That's the table-stakes behavior you actually want from a retrieval tool, and it's worth more than a fabricated 1-step saving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-validation with CodeGraph's published 7-repo benchmark
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Published (7 repos)&lt;/th&gt;
&lt;th&gt;This test (Hono, n=4)&lt;/th&gt;
&lt;th&gt;Reproduces?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool calls&lt;/td&gt;
&lt;td&gt;−71%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−55%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ Yes — same ballpark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;−46%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−20%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~ Directionally, ~half the magnitude&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;td&gt;−57%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−23%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~ Directionally, smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−35%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+6.8%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✗ &lt;strong&gt;No — opposite sign&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tool-call reduction is the part that generalizes cleanly to a repo the team didn't pick. The cost reduction is the part that doesn't — and that's not an attack on CodeGraph, it's a statement about repo size. Their published suite skews large (VS Code is 30k+ files; Tokio is mid-thousands), and their own published table is &lt;strong&gt;non-monotonic in file count&lt;/strong&gt; — Gin (~110 files) shows a 21% cost win while OkHttp (~645 files) shows ~2%, and Tokio (~790 files) shows 82%. &lt;strong&gt;Repo size matters, but it isn't a clean threshold; question shape matters at least as much.&lt;/strong&gt; A single repo can't locate a universal crossover. What Hono shows is one clear data point: at ~280 files, the step-count win is already here, the dollar win isn't yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix — install CodeGraph when
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Install CodeGraph&lt;/th&gt;
&lt;th&gt;Skip (baseline grep+Read is enough)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You care about &lt;strong&gt;fewer agent steps / lower latency&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✓ (−55% tool calls even on a small repo)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You're optimizing &lt;strong&gt;dollar cost&lt;/strong&gt; on a sub-~500-file repo&lt;/td&gt;
&lt;td&gt;(may cost slightly more)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo is large (low thousands of files+)&lt;/td&gt;
&lt;td&gt;✓ (dollar win should appear too)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workload is &lt;strong&gt;broad multi-file navigation / architecture&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✓ (this is where it wins on every axis)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workload is &lt;strong&gt;narrow single-symbol lookups&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;(fewer steps, but not cheaper)&lt;/td&gt;
&lt;td&gt;(grep is fine)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static-typed (TS / Rust / Go / Java / Swift / C#)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic-typed (Python / Ruby / untyped JS)&lt;/td&gt;
&lt;td&gt;⚠️ partial (tree-sitter misses runtime semantics)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow is text-search dominant&lt;/td&gt;
&lt;td&gt;(no penalty — the agent declines the tool)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent &lt;strong&gt;reliability&lt;/strong&gt; matters (bounding worst-case exploration)&lt;/td&gt;
&lt;td&gt;✓ (caps the 50-tool-call spirals)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You're not sure&lt;/td&gt;
&lt;td&gt;install it; ~10 min, &amp;lt;2s to index a Hono-sized repo, uninstall is one command&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key call from the data:&lt;/strong&gt; at Hono's scale the reason to install CodeGraph is &lt;strong&gt;fewer steps, lower latency, and bounded worst-case exploration&lt;/strong&gt; — not a lower bill. If your decision rule is purely dollars-per-query on a small repo, baseline grep+Read is still competitive. If it's agent speed, predictability, or you're working in a larger codebase, the index earns its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd want to test next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Larger TS repo head-to-head&lt;/strong&gt; — same 5 questions on Prisma (~2,000 TS files) or TanStack Query (~600 files) to find where the &lt;em&gt;dollar&lt;/em&gt; crossover actually is. Hypothesis: cost flips negative somewhere in the high hundreds to low thousands of files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic-typed repo&lt;/strong&gt; — same 5 questions on FastAPI or Django REST to see how much of the step-count win survives when tree-sitter can't resolve dynamic dispatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-session compounding&lt;/strong&gt; — single-question runs miss the multi-turn agent context. Does the per-query step saving compound across a real session, or stay linear?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All future content. None block the install-or-not decision the data above already answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  One-line verdict
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;On TypeScript / Rust / Go projects, install CodeGraph if you want fewer agent steps, lower latency, and bounded worst-case exploration — those reproduce on an independent repo. Don't install it expecting a lower bill on a small codebase: at Hono's ~280-file scale it was ~7% &lt;em&gt;more&lt;/em&gt; expensive, and in this benchmark a cost win appeared only on broad multi-file navigation (Q3) — the published −35% likely needs much larger repos.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the architectural deep-dive on &lt;em&gt;why&lt;/em&gt; this class of tool works and where the abstractions leak, see the companion Lab piece &lt;em&gt;Agent Retrieval Above the Crossover: A First-Principles Read of CodeGraph&lt;/em&gt; (publishing 2026-06-08).&lt;/p&gt;

&lt;p&gt;For the broader cost-curve framework this benchmark applies, see the prior Lab post: &lt;em&gt;&lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;Agent Retrieval Is a Cost Curve Problem&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: Benchmark Questions
&lt;/h2&gt;

&lt;p&gt;The 5 prompts used, verbatim. Each was sent to Claude Code in a fresh session (&lt;code&gt;claude --print --model opus&lt;/code&gt;), 4 times per arm. No follow-up prompts within a run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q1 — Route resolution
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;When a request hits &lt;code&gt;GET /users/:id&lt;/code&gt; in a Hono app, walk me through how Hono's routing finds and invokes the right handler. Where in the source does the URL → handler matching happen, and what data structure stores the route table?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Q2 — Middleware chain trace
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Hono middleware is chained via &lt;code&gt;app.use(middleware)&lt;/code&gt;. When a request flows through several middlewares before hitting the handler, what's the actual call stack from the incoming request to the handler? Specifically — how does Hono ensure middleware runs in order, and how is &lt;code&gt;c&lt;/code&gt; (context) + &lt;code&gt;next&lt;/code&gt; passed between them?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Q3 — Cross-file abstraction navigation
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Hono supports multiple runtime adapters (Cloudflare Workers, Deno, Bun, Node, AWS Lambda, Vercel Edge). How is this multi-runtime abstraction implemented? What's the shared interface, and where do the per-runtime adapters live? Show me the architecture.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Q4 — Refactor impact
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Imagine I'm planning to add a mandatory new property &lt;code&gt;requestId: string&lt;/code&gt; to Hono's &lt;code&gt;Context&lt;/code&gt; class. What files and functions across the codebase would be affected? Give me the full blast radius — where Context is constructed, where it's typed in signatures, and where mandatory-property additions would break.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Q5 — Text search (control)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Find every place in the Hono codebase where the literal string &lt;code&gt;'Content-Type'&lt;/code&gt; (the exact HTTP header name, case-sensitive) appears. Include source code, tests, comments, and documentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Scoring &amp;amp; environment
&lt;/h3&gt;

&lt;p&gt;Each question evaluated on cost (&lt;code&gt;total_cost_usd&lt;/code&gt;), tokens (input+output, cache tracked separately), wall latency, unique tool-call count, and a manual correctness/completeness check (both arms agreed on the same answer in every Q). Every CodeGraph run was additionally checked for &lt;code&gt;connected&lt;/code&gt; MCP status and actual &lt;code&gt;codegraph_*&lt;/code&gt; tool invocation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt; Hono @ commit &lt;code&gt;2cbeadda&lt;/code&gt; (2026-05-28) · CodeGraph 0.9.7 · Claude Code 2.1.159 · model &lt;code&gt;claude-opus-4-8&lt;/code&gt; (Opus 4.8) · macOS. &lt;strong&gt;Raw per-run data&lt;/strong&gt; (cost, tokens, tool calls, latency, connection status) for all 40 runs: &lt;strong&gt;&lt;a href="https://harrisonsec.com/codegraph-hono-benchmark-data.csv" rel="noopener noreferrer"&gt;CSV&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Appendix: Why there's no third arm (knowing)
&lt;/h2&gt;

&lt;p&gt;I intended to include &lt;strong&gt;knowing&lt;/strong&gt; (Blackwell Systems) as a third arm. In headless batch mode its MCP server connected only intermittently — knowing advertises asynchronous &lt;code&gt;tools/listChanged&lt;/code&gt;, which races Claude Code's MCP startup window, so on most runs the agent never saw knowing's tools and silently fell back to grep+Read.&lt;/p&gt;

&lt;p&gt;Reporting task-cost numbers from runs where the tool wasn't actually in the loop is exactly the trap that invalidated my &lt;em&gt;first&lt;/em&gt; attempt at this benchmark (see Verifying the tool actually ran), so I'm not publishing knowing figures. That's a limitation of my batch harness, &lt;strong&gt;not&lt;/strong&gt; a verdict on knowing — a persistent / pre-warmed MCP host or an interactive session would likely fix it. If I get a reliable knowing setup, I'll benchmark it on its own terms and publish separately.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Lab companion (first-principles architectural read of CodeGraph and the class of tools it represents): **Agent Retrieval Above the Crossover&lt;/em&gt;* — publishing 2026-06-08.*&lt;br&gt;
&lt;em&gt;Prior Lab post in the retrieval / memory mini-series: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;Agent Retrieval Is a Cost Curve Problem&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;Companion Lab post on cross-session memory: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/agent-memory-is-a-cache-coherence-problem/" rel="noopener noreferrer"&gt;Agent Memory Is a Cache Coherence Problem&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;CodeGraph repo: *&lt;/em&gt;&lt;a href="https://github.com/colbymchenry/codegraph" rel="noopener noreferrer"&gt;https://github.com/colbymchenry/codegraph&lt;/a&gt;***&lt;/p&gt;

</description>
      <category>ai</category>
      <category>benchmark</category>
      <category>devtools</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Agent Memory Is a Cache Coherence Problem</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Fri, 29 May 2026 15:17:06 +0000</pubDate>
      <link>https://dev.to/harrisonsec/agent-memory-is-a-cache-coherence-problem-4jmk</link>
      <guid>https://dev.to/harrisonsec/agent-memory-is-a-cache-coherence-problem-4jmk</guid>
      <description>&lt;p&gt;This post is one half of a pair. The other half — &lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;&lt;em&gt;Agent Retrieval Is a Cost Curve Problem&lt;/em&gt;&lt;/a&gt; — argues that Claude Code's within-session code retrieval avoids RAG because the cost curve says it should. This piece argues something parallel about &lt;em&gt;cross-session memory&lt;/em&gt;: the lossy auto-capture systems being marketed as "AI memory" are, in classical distributed-systems vocabulary, &lt;strong&gt;caches&lt;/strong&gt;. They inherit every problem caches have always had, and the hype around them is mostly arguing for one side of a write-back vs write-through trade as if the other side didn't exist.&lt;/p&gt;

&lt;p&gt;Sequel to &lt;em&gt;&lt;a href="https://harrisonsec.com/blog/consistency-scenarios-and-approaches-production/" rel="noopener noreferrer"&gt;Consistency in Distributed Systems: Scenarios, Trade-offs, and What Actually Works&lt;/a&gt;&lt;/em&gt;. If you remember that piece, you'll recognize the move: take a problem space the AI community is debating with fresh vocabulary, and notice that the database community already mapped the failure modes thirty years ago.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — Cross-session agent memory varies on two independent axes, not one: &lt;strong&gt;fidelity&lt;/strong&gt; (lossless vs lossy) and &lt;strong&gt;retrieval&lt;/strong&gt; (exact lookup vs approximate vector). Claude Code's built-in memory plus a hand-written &lt;code&gt;CLAUDE.md&lt;/code&gt; lives at &lt;em&gt;lossless + exact&lt;/em&gt;. The currently-trending &lt;code&gt;claude-mem&lt;/code&gt; (70k+ GitHub stars as of May 2026) lives at &lt;em&gt;lossy + approximate&lt;/em&gt; — auto-capture passed through a Haiku compression step and recalled via SQLite-FTS5 + Chroma vectors. The second is, structurally, a cache: a derived lossy view of the source of truth, retrieved approximately. It inherits every cache problem the distributed-systems literature already named — staleness, wrong-row retrieval, no coherence with the source. I ran claude-mem under controlled conditions and compared it against the deterministic CLAUDE.md baseline; the numbers (and the &lt;em&gt;kinds&lt;/em&gt; of failures) line up with the classical cache framing. The most interesting failure isn't tokens or latency. It's that compression flattens &lt;strong&gt;modality&lt;/strong&gt; — a hedged hypothetical becomes a flat fact, indistinguishable from a firm decision. An agent confidently acting on a maybe-it-said-yes is worse than an agent with no memory at all.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Two Axes (Don't Collapse Them Into One)
&lt;/h2&gt;

&lt;p&gt;Most takes on agent memory collapse the design space onto a single axis: "lossless and limited" vs "lossy and powerful." That framing hides the failure modes.&lt;/p&gt;

&lt;p&gt;The real space is two-dimensional:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fidelity&lt;/strong&gt; — &lt;em&gt;lossless&lt;/em&gt; (verbatim, what-you-wrote-is-what-was-stored) vs &lt;em&gt;lossy&lt;/em&gt; (LLM-compressed: a summary written by a smaller model over the raw events).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval&lt;/strong&gt; — &lt;em&gt;exact / curated&lt;/em&gt; (you wrote an index entry; the system reads it back) vs &lt;em&gt;approximate / semantic&lt;/em&gt; (vector embeddings; cosine similarity; top-K nearest neighbors).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBzdWJncmFwaCBheGVzWyJUd28gYXhlcywgZm91ciBxdWFkcmFudHMiXQogICAgICAgIGRpcmVjdGlvbiBMUgogICAgICAgIHN1YmdyYXBoIGNvbDFbIkV4YWN0IHJldHJpZXZhbCJdCiAgICAgICAgICAgIHExWyI8Yj5Mb3NzbGVzcyArIEV4YWN0PC9iPjxici8-Q0xBVURFLm1kIC8gaGFuZC1jdXJhdGVkPGJyLz5QcmVjaXNpb246IGhpZ2g8YnIvPkNvdmVyYWdlOiBsb3c8YnIvPlVwa2VlcDogbWFudWFsIl0KICAgICAgICAgICAgcTNbIjxiPkxvc3N5ICsgRXhhY3Q8L2I-PGJyLz5Db21wcmVzc2VkIGJ1dCBrZXl3b3JkLWluZGV4ZWQ8YnIvPih1bnVzdWFsIGluIHByYWN0aWNlKSJdCiAgICAgICAgZW5kCiAgICAgICAgc3ViZ3JhcGggY29sMlsiQXBwcm94aW1hdGUgcmV0cmlldmFsIl0KICAgICAgICAgICAgcTJbIjxiPkxvc3NsZXNzICsgQXBwcm94aW1hdGU8L2I-PGJyLz5SYXcgbG9ncyArIHZlY3RvciBzZWFyY2g8YnIvPihodWdlIHN0b3JhZ2U7IHdlYWsgc2lnbmFsKSJdCiAgICAgICAgICAgIHE0WyI8Yj5Mb3NzeSArIEFwcHJveGltYXRlPC9iPjxici8-PGI-Y2xhdWRlLW1lbSBhbmQgbW9zdCAnQUkgbWVtb3J5JyB0b29sczwvYj48YnIvPkF1dG8tY29tcHJlc3MgKyB2ZWN0b3IgcmVjYWxsPGJyLz5Ud28gbGF5ZXJzIG9mIGFwcHJveGltYXRpb24iXQogICAgICAgIGVuZAogICAgZW5kCgogICAgY2xhc3NEZWYgZ29vZCBmaWxsOiNmMGZmZjQsc3Ryb2tlOiMyZjg1NWEKICAgIGNsYXNzRGVmIHdhcm4gZmlsbDojZmVmNWU3LHN0cm9rZTojYjc3OTFmCiAgICBjbGFzc0RlZiBiYWQgZmlsbDojZmVkN2Q3LHN0cm9rZTojYzUzMDMwCiAgICBjbGFzcyBxMSBnb29kCiAgICBjbGFzcyBxMiB3YXJuCiAgICBjbGFzcyBxMyB3YXJuCiAgICBjbGFzcyBxNCBiYWQ%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBzdWJncmFwaCBheGVzWyJUd28gYXhlcywgZm91ciBxdWFkcmFudHMiXQogICAgICAgIGRpcmVjdGlvbiBMUgogICAgICAgIHN1YmdyYXBoIGNvbDFbIkV4YWN0IHJldHJpZXZhbCJdCiAgICAgICAgICAgIHExWyI8Yj5Mb3NzbGVzcyArIEV4YWN0PC9iPjxici8-Q0xBVURFLm1kIC8gaGFuZC1jdXJhdGVkPGJyLz5QcmVjaXNpb246IGhpZ2g8YnIvPkNvdmVyYWdlOiBsb3c8YnIvPlVwa2VlcDogbWFudWFsIl0KICAgICAgICAgICAgcTNbIjxiPkxvc3N5ICsgRXhhY3Q8L2I-PGJyLz5Db21wcmVzc2VkIGJ1dCBrZXl3b3JkLWluZGV4ZWQ8YnIvPih1bnVzdWFsIGluIHByYWN0aWNlKSJdCiAgICAgICAgZW5kCiAgICAgICAgc3ViZ3JhcGggY29sMlsiQXBwcm94aW1hdGUgcmV0cmlldmFsIl0KICAgICAgICAgICAgcTJbIjxiPkxvc3NsZXNzICsgQXBwcm94aW1hdGU8L2I-PGJyLz5SYXcgbG9ncyArIHZlY3RvciBzZWFyY2g8YnIvPihodWdlIHN0b3JhZ2U7IHdlYWsgc2lnbmFsKSJdCiAgICAgICAgICAgIHE0WyI8Yj5Mb3NzeSArIEFwcHJveGltYXRlPC9iPjxici8-PGI-Y2xhdWRlLW1lbSBhbmQgbW9zdCAnQUkgbWVtb3J5JyB0b29sczwvYj48YnIvPkF1dG8tY29tcHJlc3MgKyB2ZWN0b3IgcmVjYWxsPGJyLz5Ud28gbGF5ZXJzIG9mIGFwcHJveGltYXRpb24iXQogICAgICAgIGVuZAogICAgZW5kCgogICAgY2xhc3NEZWYgZ29vZCBmaWxsOiNmMGZmZjQsc3Ryb2tlOiMyZjg1NWEKICAgIGNsYXNzRGVmIHdhcm4gZmlsbDojZmVmNWU3LHN0cm9rZTojYjc3OTFmCiAgICBjbGFzc0RlZiBiYWQgZmlsbDojZmVkN2Q3LHN0cm9rZTojYzUzMDMwCiAgICBjbGFzcyBxMSBnb29kCiAgICBjbGFzcyBxMiB3YXJuCiAgICBjbGFzcyBxMyB3YXJuCiAgICBjbGFzcyBxNCBiYWQ%3D" alt="flowchart TD" width="730" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interesting failures live in the bottom-right quadrant — &lt;em&gt;lossy + approximate&lt;/em&gt; — because the failures of one axis are &lt;em&gt;invisible to a user evaluating along the other&lt;/em&gt;. The system loses information at write time and approximates at read time, and the user sees a single "answer" that fused both losses. Debugging means asking: was that wrong because the original event was corrupted in compression, or because retrieval surfaced the wrong row? You usually can't tell.&lt;/p&gt;

&lt;p&gt;Most takes conflate "lossy = unreliable" with "vector = powerful." They're orthogonal. You can have lossless + vector (raw logs, vector-indexed — fine but storage-heavy and signal-weak). You can have lossy + exact (compressed summaries, FTS-indexed — works for some applications). Lossy + approximate is what's being marketed as "AI memory," and it's the quadrant most exposed to compounding failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Baseline: Lossless + Exact (&lt;code&gt;CLAUDE.md&lt;/code&gt; + built-in memory)
&lt;/h2&gt;

&lt;p&gt;Claude Code's built-in memory system, paired with a hand-written &lt;code&gt;CLAUDE.md&lt;/code&gt;, sits firmly in the &lt;em&gt;lossless + exact&lt;/em&gt; quadrant. The design choices, made explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verbatim storage.&lt;/strong&gt; What the author wrote is what gets stored. Markdown in, Markdown out. There's no compression step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always-loaded index + on-demand body.&lt;/strong&gt; &lt;code&gt;MEMORY.md&lt;/code&gt; (the index) gets injected into every session — capped, deliberately, around 200 lines to avoid context bloat. Individual memory files are read on demand, when the index entry suggests one is relevant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curated, not a firehose.&lt;/strong&gt; A human (or a structured prompt) decides what is worth storing. Not every tool call. Not every file read. Only the durable, surprising, cross-session-useful facts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact recall.&lt;/strong&gt; The model reads a specific file. Either it's there and is read verbatim, or it isn't. No fuzzy near-matches; no confidence score.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mental model: a hand-written &lt;strong&gt;WAL&lt;/strong&gt; (write-ahead log) plus a &lt;strong&gt;curated index&lt;/strong&gt;. Both are close to a source of truth — the author's deliberate decision — and they recall exactly what was committed.&lt;/p&gt;

&lt;p&gt;Tradeoffs are visible from this framing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Precision&lt;/strong&gt;: 100%. What you stored is what you get back.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Auditability&lt;/strong&gt;: you can read the file yourself. No black box.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Token economics&lt;/strong&gt;: index sits in context; bodies fetched only when needed. Cheap.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Coverage&lt;/strong&gt;: limited to what the author bothered to write down.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Upkeep&lt;/strong&gt;: manual. Memories rot; updating them is a chore.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The earlier post in this series, &lt;a href="https://harrisonsec.com/blog/claude-code-memory-first-principles-tradeoffs/" rel="noopener noreferrer"&gt;&lt;em&gt;Claude Code Deep Dive Part 4: Why It Uses Markdown Files Instead of Vector DBs&lt;/em&gt;&lt;/a&gt;, walks through the specific design choices in the publicly circulated build snapshot. Here I'll focus on what happens when you put the lossless+exact baseline next to the lossy+approximate contender.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contender: Lossy + Approximate (&lt;code&gt;claude-mem&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/thedotmack/claude-mem" rel="noopener noreferrer"&gt;&lt;code&gt;claude-mem&lt;/code&gt;&lt;/a&gt; is among the highest-starred entries in the agent-memory category right now (70k+ GitHub stars as of May 2026). I tested v13.2.0. The architecture, summarized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-capture firehose.&lt;/strong&gt; Lifecycle hooks (&lt;code&gt;SessionStart&lt;/code&gt;, &lt;code&gt;UserPromptSubmit&lt;/code&gt;, &lt;code&gt;PostToolUse&lt;/code&gt;, &lt;code&gt;Stop&lt;/code&gt;, &lt;code&gt;SessionEnd&lt;/code&gt;) fire on essentially everything the model does. The hooks pipe events to a Bun worker on &lt;code&gt;localhost:37701&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression to facts/narrative.&lt;/strong&gt; At session boundaries the worker invokes &lt;strong&gt;Haiku 4.5&lt;/strong&gt; to compress raw observations into structured &lt;em&gt;facts&lt;/em&gt; (a JSON array) and a &lt;em&gt;narrative&lt;/em&gt; (a paragraph). This compression runs &lt;strong&gt;on your own Claude subscription&lt;/strong&gt; — billed to your quota, ~5,150 compression tokens per session in my test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid index.&lt;/strong&gt; Compressed observations are written to two indexes simultaneously: &lt;strong&gt;SQLite-FTS5&lt;/strong&gt; (full-text keyword) and &lt;strong&gt;Chroma&lt;/strong&gt; (vector embeddings). Recall is hybrid — keyword and ANN together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disables built-in memory.&lt;/strong&gt; Installation sets &lt;code&gt;CLAUDE_CODE_DISABLE_AUTO_MEMORY=1&lt;/code&gt; in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;. The built-in CLAUDE.md path is turned off; &lt;code&gt;claude-mem&lt;/code&gt; is meant to &lt;em&gt;replace&lt;/em&gt;, not augment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoded data dir.&lt;/strong&gt; Despite respecting &lt;code&gt;CLAUDE_CONFIG_DIR&lt;/code&gt; for plugin config, the data store path (&lt;code&gt;~/.claude-mem&lt;/code&gt;) is hardcoded. Sandboxing is partial.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mental model: a &lt;strong&gt;derived, lossy materialized view&lt;/strong&gt; of session events, plus a &lt;strong&gt;similarity cache&lt;/strong&gt; for retrieval. Two layers of approximation: a lossy &lt;em&gt;write&lt;/em&gt; transform (Haiku compression) and an approximate &lt;em&gt;read&lt;/em&gt; transform (ANN). Each compounds the other.&lt;/p&gt;

&lt;p&gt;This is exactly what the bottom-right quadrant looks like in deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Test (Real Numbers, 2026-05-20)
&lt;/h2&gt;

&lt;p&gt;To make the comparison concrete I built a small URL-shortener as the test bed: simple enough that the "right answer" was unambiguous, structured enough that real architectural decisions had to be recorded. The setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test arm:&lt;/strong&gt; &lt;code&gt;claude-mem&lt;/code&gt; v13.2.0, sandboxed via &lt;code&gt;CLAUDE_CONFIG_DIR=/tmp/cmem-test/dot-claude&lt;/code&gt;. Built-in memory disabled (per install default). Session 1 established four decisions about the codebase; Session 2 asked for them back. This is the arm I measured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baseline:&lt;/strong&gt; the same four decisions written into a hand-curated &lt;code&gt;CLAUDE.md&lt;/code&gt; — 1,075 chars, ~269 tokens. Built-in memory intact. The baseline numbers in the table below are deterministic properties of how the built-in &lt;code&gt;CLAUDE.md&lt;/code&gt; path works (verbatim recall, no extra round-trip, no compression bill), not a separately measured session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decisions in Session 1 (so the comparison is fair):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use Redis for URL storage.&lt;/li&gt;
&lt;li&gt;Generate short codes with base62.&lt;/li&gt;
&lt;li&gt;Add a 30-day TTL.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"We could refresh the TTL on each access if we want sliding expiration."&lt;/em&gt; (Note the hedge — this is the modality test that matters.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Numbers, side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;claude-mem (v13.2.0)&lt;/th&gt;
&lt;th&gt;bare CLAUDE.md&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per recall-cycle tokens&lt;/td&gt;
&lt;td&gt;~6,700&lt;/td&gt;
&lt;td&gt;~280&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;↳ Passive context injection on session start&lt;/td&gt;
&lt;td&gt;~1,050&lt;/td&gt;
&lt;td&gt;~269 (full file)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;↳ &lt;code&gt;mcp-search&lt;/code&gt; retrieval round-trip&lt;/td&gt;
&lt;td&gt;~502&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;↳ Haiku 4.5 compression cost (charged to your quota)&lt;/td&gt;
&lt;td&gt;~5,150 / session&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra round-trip for details&lt;/td&gt;
&lt;td&gt;yes (~22s)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fidelity of recall&lt;/td&gt;
&lt;td&gt;lossy (see below)&lt;/td&gt;
&lt;td&gt;100% verbatim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in memory state&lt;/td&gt;
&lt;td&gt;disabled (&lt;code&gt;CLAUDE_CODE_DISABLE_AUTO_MEMORY=1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;intact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression cost&lt;/td&gt;
&lt;td&gt;on user's Claude subscription&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Headless capture (&lt;code&gt;claude -p&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;zero events&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upkeep&lt;/td&gt;
&lt;td&gt;automatic&lt;/td&gt;
&lt;td&gt;manual edit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The token gap — 6,700 vs 280 — is meaningful but not the headline. The headline is the &lt;em&gt;fidelity&lt;/em&gt; row.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sharpest Failure: Compression Flattens Modality
&lt;/h2&gt;

&lt;p&gt;The four decisions written in Session 1 included three firm choices and one hedge:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We use Redis... base62 short codes... 30-day TTL... we &lt;em&gt;could&lt;/em&gt; refresh the TTL on each access &lt;strong&gt;if we want&lt;/strong&gt; sliding expiration."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When I read the raw observation row that &lt;code&gt;claude-mem&lt;/code&gt; wrote, the four items appeared as a flat JSON array — &lt;code&gt;facts: [...]&lt;/code&gt; — with &lt;strong&gt;no modal marker&lt;/strong&gt; distinguishing the hedge from the decisions. The hedge had been flattened into the same shape as the firm choices.&lt;/p&gt;

&lt;p&gt;Session 2 confirmed it. I asked the recalling agent to describe the TTL design. It cheerfully reported "we refresh the TTL on each access for sliding expiration" — as though that had been decided. When I challenged it directly, its own reply was that it could not distinguish firm decisions from options that had merely been considered. The compressed &lt;code&gt;facts[]&lt;/code&gt; row it was reading from preserved the content of each item but not its modal status — what I'll call its &lt;em&gt;epistemic status&lt;/em&gt; throughout the rest of this post.&lt;/p&gt;

&lt;p&gt;That's the failure. &lt;strong&gt;The lossy layer loses epistemic status, not just bytes.&lt;/strong&gt; A &lt;em&gt;maybe&lt;/em&gt; becomes a &lt;em&gt;decision&lt;/em&gt;. The recalling agent has no way to know it shouldn't trust the row.&lt;/p&gt;

&lt;p&gt;This is worse than no memory. An agent with no memory has to ask, or reread, or check the code. An agent with confident-wrong memory acts. The cost of acting on a fabricated decision compounds: now there's code (or a PR, or an architectural note) committed under the false premise, and &lt;em&gt;that&lt;/em&gt; will be the next round's input.&lt;/p&gt;

&lt;p&gt;The generalization: any LLM compression step that maps "speech-act varieties" (decisions, hypotheses, questions, jokes, hedges) onto a single typed structure — like a &lt;code&gt;facts[]&lt;/code&gt; array — loses the modal axis. To preserve it, you'd need to compress into a &lt;em&gt;richer&lt;/em&gt; schema (with &lt;code&gt;kind: 'decision' | 'option' | 'question'&lt;/code&gt; per item), and you'd need the compression model to reliably tag the modality. Haiku 4.5 didn't tag it. Whether a more careful prompt or schema would is an open question, but it's a &lt;em&gt;design&lt;/em&gt; question the current tool doesn't even pose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six Measured Findings, Versioned to v13.2.0
&lt;/h2&gt;

&lt;p&gt;In one place, six things I measured. Versioned because tool behavior changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Replaces, not augments.&lt;/strong&gt; Install sets &lt;code&gt;CLAUDE_CODE_DISABLE_AUTO_MEMORY=1&lt;/code&gt;. Built-in memory is turned off. Default deployment is single-system, not hybrid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial sandbox.&lt;/strong&gt; &lt;code&gt;CLAUDE_CONFIG_DIR&lt;/code&gt; redirects plugin config, but the data store path &lt;code&gt;~/.claude-mem&lt;/code&gt; is hardcoded. Multi-tenant isolation is incomplete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression runs on your subscription.&lt;/strong&gt; Haiku 4.5 compresses observations to ~5,150 tokens per session, billed to &lt;em&gt;your&lt;/em&gt; Anthropic quota. Free tools that consume your paid quota deserve a footnote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invisible to headless mode.&lt;/strong&gt; &lt;code&gt;claude -p&lt;/code&gt; runs (non-interactive) emit &lt;em&gt;zero&lt;/em&gt; capture events in my tests. The lifecycle hooks fire only in interactive sessions. CI users and automation pipelines get no memory at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression flattens modality&lt;/strong&gt; (the sharpest finding, detailed above). A hedge becomes a flat fact, indistinguishable from a firm decision in the compressed &lt;code&gt;facts[]&lt;/code&gt; schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token economics lose on small projects.&lt;/strong&gt; ~6,700 tokens per recall cycle (passive inject + mcp-search round-trip + Haiku compression) versus ~280 deterministic, 100%-faithful tokens for the CLAUDE.md baseline. On a 1,000-line project, the per-token cost gap is wider than any retrieval benefit it provides.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tool-version drift is real — by the time you read this, some of these may have been fixed. The cache-coherence framing in the next section is version-independent and was the actual reason I wrote the post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is a Cache Problem, Precisely
&lt;/h2&gt;

&lt;p&gt;The distributed-systems vocabulary for this design is &lt;em&gt;materialized view of a source&lt;/em&gt;, &lt;em&gt;served from a similarity cache&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write-time lossy transform&lt;/strong&gt; = a materialized view that can drift from the source of truth (the actual codebase, the actual decisions). The source is the user's intent and the live code; the view is the compressed facts/narrative. Each write step can lose information that the view will never recover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-time ANN&lt;/strong&gt; = approximate retrieval. Top-K nearest neighbors. False positives are structural, not a bug — a sufficiently-similar wrong row will be returned with confidence indistinguishable from the right one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No coherence with the source.&lt;/strong&gt; Classical caches have &lt;strong&gt;invalidation hooks&lt;/strong&gt; — write-through, write-back, snoop protocols, MESI states. They tie cache lines back to the canonical source so that writes propagate and stale lines get evicted or rewritten. &lt;code&gt;claude-mem&lt;/code&gt; has &lt;em&gt;no&lt;/em&gt; tie to the codebase or to user-issued corrections. You reverse a decision in conversation, the memory still believes the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staleness without expiry.&lt;/strong&gt; Even without explicit invalidation, classical caches use TTLs to bound staleness. &lt;code&gt;claude-mem&lt;/code&gt; has no TTL on facts. A fact written six months ago competes for retrieval with one written yesterday, and the older one might win the vector hop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the cache-coherence frame is the right frame, the literature is rich and useful. Pat Helland's &lt;em&gt;Immutability Changes Everything&lt;/em&gt; (ACM Queue, 2015) and the broader databases-and-OS literature on cache-coherence protocols (MESI / MOESI), materialized-view invalidation, and write-through vs write-back are the right starting reading. The trades they describe — staleness vs cost, eventual vs strong coherence, when to flush, when to invalidate — are the &lt;em&gt;same trades&lt;/em&gt; the agent-memory community is rediscovering with fresh names.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBzcmNbIlNvdXJjZSBvZiB0cnV0aCJdCiAgICAgICAgczFbIlVzZXIgaW50ZW50PGJyLz5MaXZlIGNvZGU8YnIvPkNvbnZlcnNhdGlvbiJdCiAgICBlbmQKICAgIHN1YmdyYXBoIHdyaXRlWyJXcml0ZSBwYXRoIChsb3NzeSkiXQogICAgICAgIHcxWyJIYWlrdSA0LjU8YnIvPmNvbXByZXNzaW9uIl0KICAgICAgICB3MlsiZmFjdHNbXSDCtyBuYXJyYXRpdmU8YnIvPihubyBtb2RhbGl0eSwgbm8gcHJvdmVuYW5jZSkiXQogICAgZW5kCiAgICBzdWJncmFwaCBzdG9yZVsiTWF0ZXJpYWxpemVkIHZpZXciXQogICAgICAgIHN0MVsiU1FMaXRlIEZUUzUiXQogICAgICAgIHN0MlsiQ2hyb21hIHZlY3RvcnMiXQogICAgZW5kCiAgICBzdWJncmFwaCByZWFkWyJSZWFkIHBhdGggKGFwcHJveGltYXRlKSJdCiAgICAgICAgcjFbInF1ZXJ5IHZlY3Rvcjxici8-KyBrZXl3b3JkIl0KICAgICAgICByMlsidG9wLUsgaGl0cyJdCiAgICBlbmQKCiAgICBzMSAtLT58aG9vayBjYXB0dXJlc3wgdzEKICAgIHcxIC0tPiB3MgogICAgdzIgLS0-IHN0MQogICAgdzIgLS0-IHN0MgogICAgc3QxIC0tPiByMgogICAgc3QyIC0tPiByMgogICAgcjEgLS0-IHIyCiAgICByMiAtLT58ImluamVjdGVkIGFzIGZhY3QifCBzMQoKICAgIGNsYXNzRGVmIHNyYyBmaWxsOiNlNmZmZmEsc3Ryb2tlOiMzMTk3OTUKICAgIGNsYXNzRGVmIGxvc3N5IGZpbGw6I2ZlZDdkNyxzdHJva2U6I2M1MzAzMAogICAgY2xhc3NEZWYgc3RvcmUgZmlsbDojZmVmNWU3LHN0cm9rZTojYjc3OTFmCiAgICBjbGFzc0RlZiByZWFkIGZpbGw6I2ViZjRmZixzdHJva2U6IzVhNjdkOAogICAgY2xhc3MgczEgc3JjCiAgICBjbGFzcyB3MSx3MiBsb3NzeQogICAgY2xhc3Mgc3QxLHN0MiBzdG9yZQogICAgY2xhc3MgcjEscjIgcmVhZA%3D%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBzcmNbIlNvdXJjZSBvZiB0cnV0aCJdCiAgICAgICAgczFbIlVzZXIgaW50ZW50PGJyLz5MaXZlIGNvZGU8YnIvPkNvbnZlcnNhdGlvbiJdCiAgICBlbmQKICAgIHN1YmdyYXBoIHdyaXRlWyJXcml0ZSBwYXRoIChsb3NzeSkiXQogICAgICAgIHcxWyJIYWlrdSA0LjU8YnIvPmNvbXByZXNzaW9uIl0KICAgICAgICB3MlsiZmFjdHNbXSDCtyBuYXJyYXRpdmU8YnIvPihubyBtb2RhbGl0eSwgbm8gcHJvdmVuYW5jZSkiXQogICAgZW5kCiAgICBzdWJncmFwaCBzdG9yZVsiTWF0ZXJpYWxpemVkIHZpZXciXQogICAgICAgIHN0MVsiU1FMaXRlIEZUUzUiXQogICAgICAgIHN0MlsiQ2hyb21hIHZlY3RvcnMiXQogICAgZW5kCiAgICBzdWJncmFwaCByZWFkWyJSZWFkIHBhdGggKGFwcHJveGltYXRlKSJdCiAgICAgICAgcjFbInF1ZXJ5IHZlY3Rvcjxici8-KyBrZXl3b3JkIl0KICAgICAgICByMlsidG9wLUsgaGl0cyJdCiAgICBlbmQKCiAgICBzMSAtLT58aG9vayBjYXB0dXJlc3wgdzEKICAgIHcxIC0tPiB3MgogICAgdzIgLS0-IHN0MQogICAgdzIgLS0-IHN0MgogICAgc3QxIC0tPiByMgogICAgc3QyIC0tPiByMgogICAgcjEgLS0-IHIyCiAgICByMiAtLT58ImluamVjdGVkIGFzIGZhY3QifCBzMQoKICAgIGNsYXNzRGVmIHNyYyBmaWxsOiNlNmZmZmEsc3Ryb2tlOiMzMTk3OTUKICAgIGNsYXNzRGVmIGxvc3N5IGZpbGw6I2ZlZDdkNyxzdHJva2U6I2M1MzAzMAogICAgY2xhc3NEZWYgc3RvcmUgZmlsbDojZmVmNWU3LHN0cm9rZTojYjc3OTFmCiAgICBjbGFzc0RlZiByZWFkIGZpbGw6I2ViZjRmZixzdHJva2U6IzVhNjdkOAogICAgY2xhc3MgczEgc3JjCiAgICBjbGFzcyB3MSx3MiBsb3NzeQogICAgY2xhc3Mgc3QxLHN0MiBzdG9yZQogICAgY2xhc3MgcjEscjIgcmVhZA%3D%3D" alt="flowchart LR" width="1357" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's missing from this diagram, and crucially: a backedge from "source of truth" to the materialized view that fires when the source changes. That's the &lt;strong&gt;invalidation arrow&lt;/strong&gt;. Its absence is the structural reason &lt;code&gt;claude-mem&lt;/code&gt; gets wrong-row retrieval on decisions the user has reversed. Until something supplies that arrow, the system is best understood as a write-only cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Each Wins
&lt;/h2&gt;

&lt;p&gt;Cost-curve thinking (the same frame used in the &lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;companion piece&lt;/a&gt;) gives a clean answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lossless + Exact wins when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project size is small or scope is clear.&lt;/strong&gt; Curation is cheap; the manual upkeep budget is small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fidelity matters.&lt;/strong&gt; You need to recall the &lt;em&gt;exact&lt;/em&gt; decision, not a vibe of it. Coding agents, design decisions, security policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The author exists and is engaged.&lt;/strong&gt; Someone is willing to write three lines into &lt;code&gt;MEMORY.md&lt;/code&gt; when a decision is made.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lossy + Approximate wins when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;History is too big to hand-curate.&lt;/strong&gt; A year of conversations across multiple contributors, none of whom can be expected to maintain a &lt;code&gt;MEMORY.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage matters more than precision.&lt;/strong&gt; You'd rather have a fuzzy memory that something was discussed than no memory at all. Customer-support agents over a year of tickets; team retrospectives over a quarter of standups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost of acting on a fabricated fact is low.&lt;/strong&gt; A confident-wrong recall in a support agent says "sorry let me check"; the user corrects it. The same recall in a coding agent ships broken code to production. The blast radius of a false positive determines the budget for accepting one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule of thumb: &lt;strong&gt;fuzzy-but-present beats precise-but-absent&lt;/strong&gt;, but only when the false-positive cost is low enough to absorb. For coding work on a 5,000-LOC project, it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Decision Framework
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Lossless + Exact&lt;/th&gt;
&lt;th&gt;Lossy + Approximate&lt;/th&gt;
&lt;th&gt;Hybrid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single project, &amp;lt; 50k LOC&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-project / multi-year history&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decisions need exact recall&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vague-but-present recall is acceptable&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Author is engaged (willing to curate)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No human curator available&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost of confident-wrong is high (production code, money)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost of confident-wrong is low (suggestion, search)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You can pay the Haiku compression bill from your quota&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You operate headlessly or via CI&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most readers of this blog — engineers working on a single non-trivial codebase, where decisions matter and confident-wrong is expensive — the columns lean hard left.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Coherent Agent Memory System Would Need
&lt;/h2&gt;

&lt;p&gt;The interesting question, once you accept the cache-coherence frame, is: &lt;em&gt;what would the lossy + approximate corner look like if it were built like a real cache instead of a write-only one?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A short list of capabilities the current generation of "AI memory" tools is missing, and which any serious system in this space will eventually have to ship:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source pointer (provenance).&lt;/strong&gt; Every fact carries a back-pointer to the originating event: timestamp, session ID, the raw transcript turn or tool result it was derived from. Without this, you can't audit a wrong recall — you only see the fact, never its lineage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modality tagging.&lt;/strong&gt; Every fact tagged with epistemic status — &lt;code&gt;decision | option_considered | hypothesis | question | observation&lt;/code&gt; — at write time, by the compression model. Without this, the system loses what the failure section above showed: the difference between &lt;em&gt;we will&lt;/em&gt; and &lt;em&gt;we could&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supersedes / invalidation chain.&lt;/strong&gt; A later fact can declare an earlier fact superseded ("decision A was reversed on date T by B"). Recall surfaces the &lt;em&gt;latest applicable&lt;/em&gt; fact, not the most semantically similar one. This is the in-band invalidation classical caches use; agent memory currently has none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiry / TTL by class.&lt;/strong&gt; Decisions might be permanent; observations rot fast ("the build was passing this morning" should not influence behavior at 4 PM). Different fact classes get different TTLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalidation hook tied to the source of truth.&lt;/strong&gt; When the underlying codebase or document changes in a way that contradicts a stored fact, the fact gets flagged for re-validation. This is the &lt;em&gt;write-through&lt;/em&gt; arrow in the cache diagram earlier — currently absent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence surfaced to the caller.&lt;/strong&gt; Instead of returning a flat string, return &lt;code&gt;{value, confidence, provenance}&lt;/code&gt;. The recalling agent then knows when to trust, when to double-check, when to ignore.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these is novel. All of them are standard in production cache systems, query planners, and event-sourcing stores. They're hard to retrofit onto a system that wasn't designed with provenance and invalidation as first-class concerns. They're not hard to design in from the start — but doing so means giving up the "just drop a hook on everything, ship next week" simplicity that makes the current crop of tools accumulate stars.&lt;/p&gt;

&lt;p&gt;If you're building an agent that has to &lt;em&gt;act&lt;/em&gt; on its memory rather than just &lt;em&gt;display&lt;/em&gt; it, this list is the spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing — "Persistent Memory" Is a Trade, Not a Feature
&lt;/h2&gt;

&lt;p&gt;"Persistent AI memory" gets talked about like a feature you turn on. It isn't. It's a choice on the &lt;em&gt;two-axis&lt;/em&gt; design space above, and every position on that space has known failure modes. The lossless-and-exact corner has the upkeep cost and the coverage limit. The lossy-and-approximate corner has the staleness, the wrong-row retrieval, and — the finding I came away most surprised by — the loss of modality.&lt;/p&gt;

&lt;p&gt;Two takeaways worth carrying out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If someone is selling you "automatic AI memory," ask which quadrant.&lt;/strong&gt; If the answer is &lt;em&gt;lossy + approximate&lt;/em&gt;, ask the six questions from the section above: provenance, modality, supersedes, TTL, invalidation, confidence. If the answer to most of them is "the embeddings handle it," you're being sold a cache wearing the word &lt;em&gt;memory&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The classical literature is the right starting point.&lt;/strong&gt; Cache coherence, write-back vs write-through, eventual vs strong consistency, materialized-view invalidation — the database and OS communities have spent forty years working through these tradeoffs. Reading their writing is more useful than reading the latest agent-memory thread.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The companion to this post — &lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;&lt;em&gt;Agent Retrieval Is a Cost Curve Problem&lt;/em&gt;&lt;/a&gt; — argues a parallel thing about &lt;em&gt;within-session&lt;/em&gt; code retrieval: that Claude Code's "use grep, not RAG" choice isn't romance ("trust the model") but math (cost curves), and that the source code shows Anthropic A/B-testing alternative retrieval architectures (Explore vs Fork) in production. Read together, the two pieces add up to a coherent stance about Anthropic's bets across the &lt;em&gt;fidelity × retrieval&lt;/em&gt; design space: in both within-session code search and cross-session memory, the default is &lt;strong&gt;lossless + exact&lt;/strong&gt;, and the alternative branches are kept gated behind feature flags so the decisions can flip when the cost curves do. The memory side alone has at least four such gates visible in the snapshot I reviewed — &lt;code&gt;tengu_coral_fern&lt;/code&gt;, &lt;code&gt;tengu_herring_clock&lt;/code&gt;, &lt;code&gt;tengu_passport_quail&lt;/code&gt;, &lt;code&gt;tengu_slate_thimble&lt;/code&gt; — plus the build-time &lt;code&gt;KAIROS&lt;/code&gt;, &lt;code&gt;TEAMMEM&lt;/code&gt;, and &lt;code&gt;EXTRACT_MEMORIES&lt;/code&gt; gates in &lt;code&gt;src/memdir/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That stance is not "we trust the model." It's: read the cost curves, build for the curves' current shape, leave the toggles in for when they shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: How I Measured This
&lt;/h2&gt;

&lt;p&gt;For the reader who wants to reproduce — or, more usefully, who wants to know exactly what was and wasn't measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Versions and environment.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;claude-mem&lt;/code&gt; v13.2.0 (npm install via the project's standard install script)&lt;/li&gt;
&lt;li&gt;Claude Code: current public release at time of test (2026-05-20)&lt;/li&gt;
&lt;li&gt;macOS 25.4.0, zsh&lt;/li&gt;
&lt;li&gt;Sandbox: &lt;code&gt;CLAUDE_CONFIG_DIR=/tmp/cmem-test/dot-claude&lt;/code&gt; for the plugin config; data store at &lt;code&gt;~/.claude-mem&lt;/code&gt; (this directory location is hardcoded inside the tool — see Finding #2)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test project.&lt;/strong&gt;&lt;br&gt;
A small URL-shortener spec written from scratch, with four decisions in Session 1: (1) Redis for URL storage, (2) base62 short-code generation, (3) 30-day TTL, (4) the hedged sliding-expiration option that became the modality test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commands and protocol.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fresh &lt;code&gt;claude-mem&lt;/code&gt; install into the sandboxed &lt;code&gt;CLAUDE_CONFIG_DIR&lt;/code&gt;. Verified install behavior set &lt;code&gt;CLAUDE_CODE_DISABLE_AUTO_MEMORY=1&lt;/code&gt; in &lt;code&gt;settings.json&lt;/code&gt; (Finding #1).&lt;/li&gt;
&lt;li&gt;Session 1: interactive Claude Code session, walked through the four decisions with the tool actively capturing via lifecycle hooks. Watched the Bun worker on &lt;code&gt;localhost:37701&lt;/code&gt; accept events.&lt;/li&gt;
&lt;li&gt;Session ended; session-boundary compression fired; Haiku-compressed &lt;code&gt;facts[]&lt;/code&gt; and &lt;code&gt;narrative&lt;/code&gt; written to SQLite-FTS5 + Chroma. Token count for the compression call read from the API trace.&lt;/li&gt;
&lt;li&gt;Session 2: fresh interactive session; queried for each of the four decisions; observed the recall path (mcp-search round-trip; ~22s extra latency).&lt;/li&gt;
&lt;li&gt;Compared retrieved content against the original Session 1 transcript byte-by-byte to identify the modality flattening (Finding #5).&lt;/li&gt;
&lt;li&gt;Repeated the install + Session 1 pattern in headless &lt;code&gt;claude -p&lt;/code&gt; mode to confirm Finding #4 (no events captured).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What I'm explicitly &lt;em&gt;not&lt;/em&gt; claiming.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is a single test run on a deliberately small project. The N is 1.&lt;/li&gt;
&lt;li&gt;The CLAUDE.md baseline column in the test table is &lt;em&gt;not&lt;/em&gt; a separately measured comparison session. It reflects deterministic properties of the built-in &lt;code&gt;CLAUDE.md&lt;/code&gt; path (verbatim recall, no compression bill, no extra round-trip) that follow from the design — not a measured outcome.&lt;/li&gt;
&lt;li&gt;I didn't benchmark Chroma vector recall quality across many queries. The modality finding came from a single targeted probe (the TTL question); the cache-coherence framing predicts the same class of failure across many queries, but predicting and measuring are different.&lt;/li&gt;
&lt;li&gt;I tested v13.2.0. The tool is actively developed; specific findings may have been addressed in later releases by the time you read this. The cache-coherence framing is version-independent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Artifacts.&lt;/strong&gt; Session 1 / Session 2 transcripts, the raw &lt;code&gt;claude-mem&lt;/code&gt; SQLite + Chroma snapshots, and the token-cost API traces from the test run are kept locally and can be made available on reasonable request — get in touch.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Companion piece: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/agent-retrieval-cost-curve-claude-code-grep-vs-rag/" rel="noopener noreferrer"&gt;Agent Retrieval Is a Cost Curve Problem: Why Claude Code Doesn't Use RAG&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;Background: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/consistency-scenarios-and-approaches-production/" rel="noopener noreferrer"&gt;Consistency in Distributed Systems: Scenarios, Trade-offs, and What Actually Works&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;For the design rationale behind Claude Code's built-in memory in particular: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/claude-code-memory-first-principles-tradeoffs/" rel="noopener noreferrer"&gt;Claude Code Deep Dive Part 4: Why It Uses Markdown Files Instead of Vector DBs&lt;/a&gt;***&lt;/p&gt;

</description>
      <category>ai</category>
      <category>memory</category>
      <category>claudecode</category>
      <category>programming</category>
    </item>
    <item>
      <title>Agent Retrieval Is a Cost Curve Problem: Why Claude Code Doesn't Use RAG</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Tue, 26 May 2026 03:12:20 +0000</pubDate>
      <link>https://dev.to/harrisonsec/agent-retrieval-is-a-cost-curve-problem-why-claude-code-doesnt-use-rag-5c6m</link>
      <guid>https://dev.to/harrisonsec/agent-retrieval-is-a-cost-curve-problem-why-claude-code-doesnt-use-rag-5c6m</guid>
      <description>&lt;p&gt;There's a popular interview question making the rounds: &lt;em&gt;"Why doesn't Claude Code use RAG to retrieve code? Why grep?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The popular answer goes: chunking breaks code structure, vectors approximate when code demands exact, indexes go stale, cold-start is slow, retrieval is a black box. All five are real. None of them are the reason.&lt;/p&gt;

&lt;p&gt;They're symptoms. The reason is older than RAG, older than LLMs, older than the term &lt;em&gt;retrieval&lt;/em&gt;. It's a &lt;strong&gt;cost curve&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — Index-based retrieval pays a high build cost plus a &lt;em&gt;nonlinear&lt;/em&gt; maintenance cost in churn × index complexity. LLM tool-loop retrieval pays nothing up front and a per-query cost that's roughly project-size-independent for queries an LLM actually issues. For most small-to-mid-size repos the crossover is never reached. The "Anthropic trusts the model" framing is romantic; the actual answer is colder — build cost zero, per-query cost amortizes faster than index drift, so the math says grep.&lt;/p&gt;

&lt;p&gt;There's also a precision axis, which most engineers care about more than cost. Vector RAG is approximate by design — &lt;code&gt;getUserById&lt;/code&gt; returns alongside &lt;code&gt;getUserByEmail&lt;/code&gt; because they're semantically adjacent. Code usually wants &lt;em&gt;exact&lt;/em&gt;, which grep gives you for free. &lt;em&gt;Symbol-graph&lt;/em&gt; indexes (Sourcegraph, Kythe, LSP) are precision-first but haven't become the LLM companion either — covered below.&lt;/p&gt;

&lt;p&gt;Audited against a &lt;a href="https://harrisonsec.com/blog/claude-code-source-leaked-hidden-features/" rel="noopener noreferrer"&gt;publicly circulated build snapshot&lt;/a&gt; of Claude Code with file:line citations. The kicker: the Explore subagent's "use this when you'll need more than 3 queries" rule is gated behind a feature flag (&lt;code&gt;tengu_amber_stoat&lt;/code&gt;) and being A/B-tested against a parallel architecture (Fork). The canonical answer is conditional. &lt;em&gt;That&lt;/em&gt; is the answer that gets you the offer.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Frame That Matters: Total Cost Over Time
&lt;/h2&gt;

&lt;p&gt;Pick any retrieval system and you pay for three things, on different schedules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build cost&lt;/strong&gt; — one-time work to assemble whatever structure makes lookup fast. For an index, this is chunking + embedding + insert. For tool-loops, this is zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintain cost&lt;/strong&gt; — ongoing work to keep the structure honest as the underlying data changes. For an index, this is invalidation, reindex, drift reconciliation. For tool-loops, this is also zero — the "structure" is the live filesystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-query cost&lt;/strong&gt; — work done when a question arrives. For an index, this is a vector search + a few reranks + an LLM call. For tool-loops, this is N LLM-tool round-trips, where N varies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The temptation is to compare per-query cost: "vector search is one round-trip, tool-loop is six." That's why RAG looks dominant on a whiteboard. But you ship a system, not a whiteboard. The bill is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;total_cost = build_cost + maintain_cost × time + per_query_cost × queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a project that changes daily and gets queried hourly, the term that actually grows is &lt;code&gt;maintain_cost × time&lt;/code&gt;. For index-based retrieval on a churning codebase, maintain cost grows at least linearly with churn — and &lt;em&gt;can&lt;/em&gt; grow faster than teams expect, because cross-chunk and cross-file references force cascading re-embeddings and symbol-graph consistency checks. A naive incremental indexer is linear; a correct one tracking cross-file refactors is often worse than linear in the worst case. For tool-loops, maintain cost is identically zero, because the loop has no persistent structure.&lt;/p&gt;

&lt;p&gt;The build/maintain term dominates anything you save on per-query cost, until your project is large enough that per-query cost itself becomes the bottleneck. For most small-to-mid-size repos, the crossover is never reached.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBzbWFsbFsiTW9zdCBwcm9qZWN0cyDCtyBncmVwIHdpbnMiXQogICAgICAgIHMxWyJidWlsZDogMDxici8-bWFpbnRhaW46IDA8YnIvPnF1ZXJ5OiB-TiDDlyB0b29sIHJvdW5kLXRyaXAiXQogICAgZW5kCiAgICBzdWJncmFwaCBjcm9zc292ZXJbIkNyb3Nzb3ZlciDCtyB3b3JrbG9hZC1kZXBlbmRlbnQiXQogICAgICAgIGMxWyJpbmRleCBidWlsZCBjb3N0PGJyLz5hbW9ydGl6ZXMgd2hlbjxici8-cXVlcnkgcmF0ZSDDlyBwcmVjaXNpb24gc2F2aW5nczxici8-ZXhjZWVkcyBtYWludGFpbiBjb3N0Il0KICAgIGVuZAogICAgc3ViZ3JhcGggaHVnZVsiTWVnYS1tb25vcmVwbyDCtyBpbmRleCB3aW5zIl0KICAgICAgICBoMVsiYnVpbGQ6IGhpZ2g8YnIvPm1haW50YWluOiBoaWdoIGJ1dCBhbW9ydGl6ZWQ8YnIvPmFjcm9zcyBtaWxsaW9ucyBvZiBxdWVyaWVzPGJyLz5xdWVyeTogc2luZ2xlIHZlY3RvciBob3AiXQogICAgZW5kCgogICAgc21hbGwgLS0-IGNyb3Nzb3ZlciAtLT4gaHVnZQoKICAgIGNsYXNzRGVmIHNtYWxsIGZpbGw6I2YwZmZmNCxzdHJva2U6IzJmODU1YQogICAgY2xhc3NEZWYgY3Jvc3MgZmlsbDojZmVmNWU3LHN0cm9rZTojYjc3OTFmCiAgICBjbGFzc0RlZiBiaWcgZmlsbDojZmVkN2Q3LHN0cm9rZTojYzUzMDMwCiAgICBjbGFzcyBzbWFsbCBzbWFsbAogICAgY2xhc3MgY3Jvc3NvdmVyIGNyb3NzCiAgICBjbGFzcyBodWdlIGJpZw%3D%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBzbWFsbFsiTW9zdCBwcm9qZWN0cyDCtyBncmVwIHdpbnMiXQogICAgICAgIHMxWyJidWlsZDogMDxici8-bWFpbnRhaW46IDA8YnIvPnF1ZXJ5OiB-TiDDlyB0b29sIHJvdW5kLXRyaXAiXQogICAgZW5kCiAgICBzdWJncmFwaCBjcm9zc292ZXJbIkNyb3Nzb3ZlciDCtyB3b3JrbG9hZC1kZXBlbmRlbnQiXQogICAgICAgIGMxWyJpbmRleCBidWlsZCBjb3N0PGJyLz5hbW9ydGl6ZXMgd2hlbjxici8-cXVlcnkgcmF0ZSDDlyBwcmVjaXNpb24gc2F2aW5nczxici8-ZXhjZWVkcyBtYWludGFpbiBjb3N0Il0KICAgIGVuZAogICAgc3ViZ3JhcGggaHVnZVsiTWVnYS1tb25vcmVwbyDCtyBpbmRleCB3aW5zIl0KICAgICAgICBoMVsiYnVpbGQ6IGhpZ2g8YnIvPm1haW50YWluOiBoaWdoIGJ1dCBhbW9ydGl6ZWQ8YnIvPmFjcm9zcyBtaWxsaW9ucyBvZiBxdWVyaWVzPGJyLz5xdWVyeTogc2luZ2xlIHZlY3RvciBob3AiXQogICAgZW5kCgogICAgc21hbGwgLS0-IGNyb3Nzb3ZlciAtLT4gaHVnZQoKICAgIGNsYXNzRGVmIHNtYWxsIGZpbGw6I2YwZmZmNCxzdHJva2U6IzJmODU1YQogICAgY2xhc3NEZWYgY3Jvc3MgZmlsbDojZmVmNWU3LHN0cm9rZTojYjc3OTFmCiAgICBjbGFzc0RlZiBiaWcgZmlsbDojZmVkN2Q3LHN0cm9rZTojYzUzMDMwCiAgICBjbGFzcyBzbWFsbCBzbWFsbAogICAgY2xhc3MgY3Jvc3NvdmVyIGNyb3NzCiAgICBjbGFzcyBodWdlIGJpZw%3D%3D" alt="flowchart LR" width="1093" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the whole argument. The rest of this post is evidence — what the cost-curve choice looks like in source code, and where Anthropic is hedging.&lt;/p&gt;

&lt;p&gt;A teaser for the punchline two sections down: the canonical "Claude Code spawns an Explore subagent for open-ended search" rule that most explainers quote is &lt;strong&gt;gated behind a feature flag&lt;/strong&gt; (&lt;code&gt;tengu_amber_stoat&lt;/code&gt;) and &lt;strong&gt;A/B-tested in production against a second architecture (Fork)&lt;/strong&gt; that takes the opposite trade. Anthropic is hedging on the retrieval design itself. We'll come back to this in &lt;em&gt;"The Subagent Twist Nobody Quotes Correctly."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Popular Answer, Charitably
&lt;/h2&gt;

&lt;p&gt;Before deflating it: the popular answer isn't wrong, it's just downstream. Briefly, with the steel-manned version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Chunking breaks structure.&lt;/strong&gt; A function split across chunks loses both halves of an &lt;code&gt;if/else&lt;/code&gt;, and call-graph relationships fragment between chunks. AST-aware chunkers exist; they're better, not solved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vectors approximate.&lt;/strong&gt; &lt;code&gt;getUserById&lt;/code&gt; returns alongside &lt;code&gt;getUserByEmail&lt;/code&gt; and &lt;code&gt;getUserByName&lt;/code&gt; because they're semantically adjacent. Exact-symbol search beats this trivially. &lt;em&gt;Important distinction:&lt;/em&gt; this is true of &lt;strong&gt;vector RAG&lt;/strong&gt; specifically. &lt;strong&gt;Symbol-graph indexes&lt;/strong&gt; — Sourcegraph, Kythe, Glean, LSP-backed code search — are a different category: they index by function/class/reference, not by chunked vector. They give exact answers and are what mega-monorepos at Google and Meta actually run for code search. When this post says "RAG," it means vector RAG. The cost-curve argument applies to vector RAG; symbol-graph has its own cost curve and lives higher up the scale spectrum. It has its own separate reason for not becoming the LLM companion — covered in the next section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexes go stale.&lt;/strong&gt; Every commit invalidates some subset of chunks. Incremental update has edge cases (renames, file moves, cross-file rename refactors). Full reindex is expensive enough to discourage frequent commits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold start.&lt;/strong&gt; Minutes-to-first-query is a non-starter for "open the tool, start working" UX.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Black-box recall.&lt;/strong&gt; Top-K vector hits are not human-auditable. When the LLM returns a wrong answer, you can't tell whether retrieval failed or reasoning did.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All five are pains. Each has a counter — better chunkers, hybrid retrieval, incremental indexers, warm pools, attribution layers. The counters cost engineering. Some teams spend the engineering and ship working systems. Anthropic looked at the bill and decided not to.&lt;/p&gt;

&lt;p&gt;Why? Because the &lt;em&gt;baseline&lt;/em&gt; for code retrieval — grep over a clean filesystem with an LLM in the loop — already works well enough that adding an index is paying engineering cost to solve symptoms whose root cause is the index itself. Removing the index removes the pains. The remaining cost is per-query LLM round-trips, which the cost-curve frame says is acceptable below the crossover.&lt;/p&gt;

&lt;p&gt;That's why grep. Everything else is engineering details on top of that decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  So Why Hasn't Symbol-Graph Become the LLM-Companion Either?
&lt;/h2&gt;

&lt;p&gt;If symbol-graph indexes are precision-first, language-aware, and battle-tested at FAANG scale, the natural question is: why didn't &lt;em&gt;they&lt;/em&gt; become the default companion to LLM coding agents? Why grep, not LSP-over-MCP?&lt;/p&gt;

&lt;p&gt;The answer is the same shape as the vector-RAG answer — high friction in places that don't show up on a feature comparison — but the specific frictions are different:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build cost is high in a different way.&lt;/strong&gt; Symbol-graph indexes need to compile (or semi-compile) the project to resolve symbols. For Rust, C++, large TypeScript or Java codebases this is minutes to tens of minutes per cold start. "Open Claude Code and start working" can't pay that toll.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language-specific, not portable.&lt;/strong&gt; LSP is one server per language. Tree-sitter coverage helps but isn't uniform. A grep-backed agent works on any text in any language with zero setup; a symbol-graph-backed agent inherits the project's language-server matrix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API/format mismatch with how LLMs reason.&lt;/strong&gt; LSP returns deeply nested JSON (locations, ranges, document hierarchies); grep returns &lt;code&gt;file:line: content&lt;/code&gt;. The second is almost literally an LLM's native dialect; the first needs adapting. The translation tax is real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage is narrower than it looks.&lt;/strong&gt; Symbol-graph models code-as-structure. It misses config files, comments, strings, generated code, markdown, env files, shell scripts, the README — all of which are first-class context for a real coding session. Grep covers anything that's text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The win is for structure questions, not intent questions.&lt;/strong&gt; "Where is &lt;code&gt;getUserById&lt;/code&gt; defined?" — symbol-graph is exactly right. "How does the login flow work?" — back to grep + read. A real coding day has both kinds; building infrastructure that only solves one kind is paying a high fixed cost for half the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The constraint reversed under it.&lt;/strong&gt; Symbol-graph was designed for a world where the constraint was &lt;em&gt;human attention bandwidth&lt;/em&gt; — give a developer one precise answer they can read. LLMs don't have that constraint; they can read 30 grep hits cheaply and reason across them. The bottleneck moved from "precision of retrieval" to "fluency of the model reading the retrieval." Symbol-graph is optimizing the part that's no longer expensive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One-line summary: &lt;strong&gt;symbol-graph is precision tooling built for human IDEs. LLMs are not human IDEs.&lt;/strong&gt; Their retrieval bottleneck is different — they prefer many cheap rounds over one expensive precise call. Installing a symbol-graph for an agent that can grep thirty times in a session is, roughly, hiring a second driver for someone who already drives.&lt;/p&gt;

&lt;p&gt;This is also why the few existing LLM ↔ symbol-graph integrations (Cursor's &lt;code&gt;@symbol&lt;/code&gt; references via LSP, Sourcegraph Cody, Codeium with LSP backend) are &lt;em&gt;additive niceties&lt;/em&gt; in those products, not the retrieval backbone. The backbone is still the same as Claude Code's — grep over text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Primitives, Audited
&lt;/h2&gt;

&lt;p&gt;Source paths below are from a publicly circulated, non-public build snapshot of Claude Code that I have on disk for analysis purposes. APIs and exact line numbers will drift; the design choices below have been stable in the snapshot I reviewed, and match observed runtime behavior on current public Claude Code releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grep — ripgrep, with structured output and a "don't shell out" enforcement clause
&lt;/h3&gt;

&lt;p&gt;The Grep tool description, from &lt;code&gt;src/tools/GrepTool/prompt.ts:7-16&lt;/code&gt;, is short and pointed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A powerful search tool built on ripgrep

  Usage:
  - ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.
  - Supports full regex syntax (e.g., "log.*Error", "function\s+\w+")
  - Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
  - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
  - Use Agent tool for open-ended searches requiring multiple rounds
  - Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use `interface\{\}` to find `interface{}` in Go code)
  - Multiline matching: By default patterns match within single lines only. For cross-line patterns like `struct \{[\s\S]*?field`, use `multiline: true`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to read out of this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The &lt;code&gt;ALWAYS / NEVER&lt;/code&gt; is doing work.&lt;/strong&gt; The model has Bash. It could shell out to &lt;code&gt;rg&lt;/code&gt; or &lt;code&gt;grep&lt;/code&gt; directly. The prompt forbids it. Why? Three reasons, in order of importance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permission surface.&lt;/strong&gt; Bash is a universal tool. Auditing what the model can do with Bash means auditing every shell command. Audited Grep means audited Grep, period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output discipline.&lt;/strong&gt; A Bash &lt;code&gt;rg&lt;/code&gt; dumps raw text into the context window. Grep returns one of three structured modes — &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;files_with_matches&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt; — letting the model pick the cheapest mode that answers the question. This is a token-budget decision, not a feature decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend swap.&lt;/strong&gt; Today the implementation is ripgrep. Tomorrow it might be &lt;code&gt;bfs&lt;/code&gt;/&lt;code&gt;ugrep&lt;/code&gt; (the source already has a branch for embedded-search tools — see &lt;code&gt;hasEmbeddedSearchTools()&lt;/code&gt; in &lt;code&gt;src/utils/embeddedTools.ts&lt;/code&gt;). A tool boundary makes the swap invisible to the model.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The default output mode is &lt;code&gt;files_with_matches&lt;/code&gt;.&lt;/strong&gt; Not &lt;code&gt;content&lt;/code&gt;. The model has to opt in to seeing actual lines. This is a token-conservation default: most of the time, the model wants to know which files matched so it can narrow further; only when it's ready to read does it ask for the lines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multiline is opt-in.&lt;/strong&gt; Default ripgrep is line-bounded — a deliberate restriction, because cross-line regex on a large tree is a perf cliff. The model can opt into multiline when it knows it needs to, paying the cost only then.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are all small choices. Each one shaves a tail off the per-query cost. Cumulatively, they're why the per-query term in our cost equation stays low enough that the build/maintain term — zero — dominates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Glob — filename patterns with a recency heuristic and a hard cap
&lt;/h3&gt;

&lt;p&gt;Glob is the "find files by name" primitive. Two design tells in its construction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Results are sorted by mtime descending.&lt;/strong&gt; Most-recently-modified file first. The heuristic is that in any given session, the files you've touched recently are the files you're about to touch again. This is the same logic IDEs use for the "Recent Files" list, and it's empirically right far more than it's wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hard cap of 100 results.&lt;/strong&gt; Past that, output truncates. The model can tighten the pattern and re-call. The cap exists because an LLM that consumes 800 file paths because it asked for &lt;code&gt;**/*&lt;/code&gt; is an LLM that's burned a quarter of its context on noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are token-budget decisions disguised as ergonomic ones. The mtime sort means a small N typically covers the relevant set. The 100-file cap means a careless query degrades gracefully instead of catastrophically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read — bounded, fresh, stat-checked
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;src/tools/FileReadTool/prompt.ts&lt;/code&gt; is the most interesting of the three because of what it constrains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- By default, it reads up to 2000 lines starting from the beginning of the file (...)
- When you already know which part of the file you need, only read that part. This can be important for larger files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;MAX_LINES_TO_READ = 2000&lt;/code&gt; at line 10; the "only read that part" guidance is &lt;code&gt;OFFSET_INSTRUCTION_TARGETED&lt;/code&gt; at line 20-21, swapped in dynamically based on context.)&lt;/p&gt;

&lt;p&gt;A 2000-line default cap. &lt;code&gt;offset&lt;/code&gt; and &lt;code&gt;limit&lt;/code&gt; parameters for targeted reads. And — critically — every read calls &lt;code&gt;stat&lt;/code&gt; on disk. No cache. No index. No staleness layer.&lt;/p&gt;

&lt;p&gt;The implication is that &lt;strong&gt;Read is always live&lt;/strong&gt;. The model that just edited a file and wants to see the result reads the file and gets the new bytes. The model that's iterating on a fix doesn't fight cache invalidation because there is no cache to invalidate. This is the "no maintain cost" term in the cost equation, made concrete: the live filesystem is the index, and the filesystem is always up to date with itself.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;OFFSET_INSTRUCTION_TARGETED&lt;/code&gt; template is worth noting separately. It's swapped in over the default when the prompting context suggests the model already knows what range it wants. It's a tiny prompt-engineering detail, but it's also a piece of evidence that the team thinks carefully about teaching the model to read selectively. The lesson the model is being taught, every prompt, is &lt;em&gt;don't be greedy&lt;/em&gt;. That's exactly the discipline that keeps per-query cost from blowing up.&lt;/p&gt;

&lt;h3&gt;
  
  
  The composition
&lt;/h3&gt;

&lt;p&gt;Walk through a real query: &lt;em&gt;"Where's the login flow in this project?"&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Glob `&lt;/strong&gt;/&lt;em&gt;login&lt;/em&gt;.{ts,tsx,js}`** — returns up to 100 files, most-recently-modified first. Usually under ten matches; usually the right one is in the first three.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grep &lt;code&gt;passport|auth|login&lt;/code&gt; --glob ''&lt;/strong&gt; — narrows to specific lines. Three output modes available; the model picks the cheapest one that disambiguates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the file, with &lt;code&gt;offset&lt;/code&gt;/&lt;code&gt;limit&lt;/code&gt; targeted at the matched region&lt;/strong&gt; — reads only what's needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three primitives. One realistic query. Total cost: three round-trips, a few hundred tokens of output total. No index, no embedding, no rebuild. Filesystem unchanged.&lt;/p&gt;

&lt;p&gt;Now add ten more iterations as the model investigates a bug. Each one is the same three primitives, in different combinations. The cost grows linearly with iterations; it doesn't grow super-linearly with the codebase. That's the curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Loop, Sketched Honestly
&lt;/h2&gt;

&lt;p&gt;You can find pseudo-code versions of Claude Code's main loop in essays and threads. They all look something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;callLLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolUses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolUses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sketch is conceptually right. It is also missing roughly 1,415 lines.&lt;/p&gt;

&lt;p&gt;The real loop is at &lt;code&gt;src/query.ts:307&lt;/code&gt; (&lt;code&gt;while (true) {&lt;/code&gt;) and closes at &lt;code&gt;:1728&lt;/code&gt; (&lt;code&gt;} // while (true)&lt;/code&gt;). The body is 1,421 lines. The full file is 1,729. If you want a guided tour, the &lt;a href="https://harrisonsec.com/blog/claude-code-deep-dive-query-loop/" rel="noopener noreferrer"&gt;Claude Code Deep Dive Part 2&lt;/a&gt; walks through it section by section. The summary for our purposes here is shorter:&lt;/p&gt;

&lt;p&gt;The Platonic loop — &lt;em&gt;call model, run tools, repeat&lt;/em&gt; — is six lines. The other 1,415 lines are doing one of four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Streaming tool execution.&lt;/strong&gt; Tool calls don't wait for the full model response before they start running; they execute as the streaming &lt;code&gt;tool_use&lt;/code&gt; blocks arrive. This is non-trivial because the model can emit a partial &lt;code&gt;tool_use&lt;/code&gt;, retract it, or continue thinking before the rest of the call arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache and compaction management.&lt;/strong&gt; Microcompact maintains tool-result cache coherence by &lt;code&gt;tool_use_id&lt;/code&gt; without inspecting content. The auto-compact path triggers when the message stream nears a context budget. Both interact with the prompt cache, which has its own coherence rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure recovery.&lt;/strong&gt; A nested &lt;code&gt;while (attemptWithFallback)&lt;/code&gt; at line 654 handles model fallback when the primary returns a recoverable error. There's a separate path for &lt;code&gt;max_output_tokens&lt;/code&gt; recovery (when the model would emit a tool_use but truncate before the &lt;code&gt;tool_result&lt;/code&gt;). Orphan tool_results are aggressively pruned so a retry doesn't carry stale IDs forward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thinking-block preservation.&lt;/strong&gt; Reasoning content has to span the assistant trajectory — same turn, plus any subsequent tool_use/tool_result chain — because the model expects to see its own prior reasoning to continue coherently. The loop preserves these blocks across iterations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The point is not "the loop is complicated." The point is: &lt;strong&gt;the cost of running an agent on tool-loops moves into the loop, where it's controllable&lt;/strong&gt;. There's no second system — no index pipeline, no vector store, no embedding service — with its own failure modes, latency budgets, and consistency guarantees. The loop is the engineering target.&lt;/p&gt;

&lt;p&gt;This is the same property that makes a single-process database easier to operate than a microservices fleet, for reasons that have nothing to do with the database. Concentrate complexity where you can attack it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Subagent Twist Nobody Quotes Correctly
&lt;/h2&gt;

&lt;p&gt;This is the section where most explanations of Claude Code's retrieval get it wrong, including the article that prompted this post. The popular telling goes: &lt;em&gt;"For open-ended exploration, Claude Code spawns an Explore subagent — Anthropic codifies the rule that if you need more than three queries, you should fork."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The rule exists. The popular telling has two things wrong about it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule is not in &lt;code&gt;AgentTool/prompt.ts&lt;/code&gt;.&lt;/strong&gt; It's in &lt;code&gt;src/constants/prompts.ts:378-379&lt;/code&gt;, injected into the system prompt by template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For simple, directed codebase searches (e.g. for a specific file/class/function) use [searchTools] directly.

For broader codebase exploration and deep research, use the Agent tool with subagent_type=Explore. This is slower than using [searchTools] directly, so use this only when a simple, directed search proves to be insufficient or when your task will clearly require more than [EXPLORE_AGENT_MIN_QUERIES] queries.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EXPLORE_AGENT_MIN_QUERIES&lt;/code&gt; is defined in &lt;code&gt;src/tools/AgentTool/built-in/exploreAgent.ts:59&lt;/code&gt; as the integer &lt;code&gt;3&lt;/code&gt;. So the "more than 3 queries" threshold is literal — but it's a single named constant, easy to change, and the guidance is interpolated dynamically per system-prompt build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule is conditional.&lt;/strong&gt; The two lines above are &lt;em&gt;only&lt;/em&gt; emitted when both conditions hold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/constants/prompts.ts:374-381&lt;/span&gt;
&lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;hasAgentTool&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="nf"&gt;areExplorePlanAgentsEnabled&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isForkSubagentEnabled&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;`For simple, directed codebase searches ...`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;`For broader codebase exploration ... subagent_type=Explore ...`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;areExplorePlanAgentsEnabled()&lt;/code&gt; and &lt;code&gt;!isForkSubagentEnabled()&lt;/code&gt; have to be true. The first is itself feature-gated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/tools/AgentTool/builtInAgents.ts:13-22&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;areExplorePlanAgentsEnabled&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BUILTIN_EXPLORE_PLAN_AGENTS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 3P default: true — Bedrock/Vertex keep agents enabled (matches pre-experiment&lt;/span&gt;
    &lt;span class="c1"&gt;// external behavior). A/B test treatment sets false to measure impact of removal.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getFeatureValue_CACHED_MAY_BE_STALE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tengu_amber_stoat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BUILTIN_EXPLORE_PLAN_AGENTS&lt;/code&gt; is a Bun build feature gate. &lt;code&gt;tengu_amber_stoat&lt;/code&gt; is a GrowthBook key. (Aside: &lt;code&gt;tengu_*&lt;/code&gt; is the Anthropic-internal naming prefix for Claude Code — any flag you see with that prefix in the source is a Claude Code feature toggle.) The comment in source is the giveaway: &lt;em&gt;"A/B test treatment sets false to measure impact of removal."&lt;/em&gt; Anthropic is &lt;strong&gt;actively testing what happens when they remove Explore and Plan agents entirely&lt;/strong&gt; for some fraction of internal users.&lt;/p&gt;

&lt;p&gt;Read that again. The canonical "Anthropic uses Explore for exploration" claim is &lt;em&gt;true for the default treatment&lt;/em&gt; and &lt;em&gt;being measured against the no-Explore baseline&lt;/em&gt;. The interview answer that says "Anthropic always uses Explore" is wrong — or at least, more confident than the source.&lt;/p&gt;

&lt;p&gt;This is the half-credit point. Knowing that the rule exists is the first half. Knowing the rule is feature-gated and under measurement is the second half.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Economics of Explore, in Detail
&lt;/h2&gt;

&lt;p&gt;If you read just the &lt;em&gt;function&lt;/em&gt; of Explore, the design looks like a generic subagent: open-ended exploration, returns a summary, isolates from the main context. If you read its &lt;em&gt;parameters&lt;/em&gt;, the economics jump out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/tools/AgentTool/built-in/exploreAgent.ts:64-83&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EXPLORE_AGENT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BuiltInAgentDefinition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;agentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Explore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;whenToUse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EXPLORE_WHEN_TO_USE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;disallowedTools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;AGENT_TOOL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// can't spawn nested subagents&lt;/span&gt;
    &lt;span class="nx"&gt;EXIT_PLAN_MODE_TOOL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;FILE_EDIT_TOOL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// read-only&lt;/span&gt;
    &lt;span class="nx"&gt;FILE_WRITE_TOOL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// read-only&lt;/span&gt;
    &lt;span class="nx"&gt;NOTEBOOK_EDIT_TOOL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;built-in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;built-in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Ants get inherit to use the main agent's model; external users get haiku for speed&lt;/span&gt;
  &lt;span class="c1"&gt;// Note: For ants, getAgentModel() checks tengu_explore_agent GrowthBook flag at runtime&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;USER_TYPE&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inherit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;haiku&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Explore is a fast read-only search agent — it doesn't need commit/PR/lint&lt;/span&gt;
  &lt;span class="c1"&gt;// rules from CLAUDE.md. The main agent has full context and interprets results.&lt;/span&gt;
  &lt;span class="na"&gt;omitClaudeMd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;getSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getExploreSystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three economic decisions encoded here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Explore runs on Haiku for external users.&lt;/strong&gt; Not the main reasoning model. Exploration is a &lt;em&gt;cheap-tokens job&lt;/em&gt; — there's no creative reasoning happening, just iterate-and-filter — and Anthropic uses a fast, small, cheap model for it. The main agent gets the expensive model when it gets the summary back. This is the &lt;em&gt;staffing&lt;/em&gt; analogue: junior associate does the deposition review, senior partner reads the brief.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore omits CLAUDE.md.&lt;/strong&gt; The argument in the inline comment: &lt;em&gt;"The main agent has full context and interprets results."&lt;/em&gt; CLAUDE.md is for project rules — commit conventions, PR style, lint guidance. Explore isn't doing any of those things. Loading CLAUDE.md into its prompt would be paying tokens for guidance it can't act on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore can't spawn subagents.&lt;/strong&gt; &lt;code&gt;AGENT_TOOL_NAME&lt;/code&gt; is in &lt;code&gt;disallowedTools&lt;/code&gt;. No recursion. This is a budget guarantee: an Explore call has bounded depth.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the system prompt makes the read-only constraint impossible to misread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
- Creating new files (no Write, touch, or file creation of any kind)
- Modifying existing files (no Edit operations)
- Deleting files (no rm or deletion)
- Moving or copying files (no mv or cp)
- Creating temporary files anywhere, including /tmp
- Using redirect operators (&amp;gt;, &amp;gt;&amp;gt;, |) or heredocs to write to files
- Running ANY commands that change system state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;exploreAgent.ts:26-34&lt;/code&gt;. The full prompt also requires Explore to spawn parallel tool calls aggressively — "you must try to spawn multiple parallel tool calls for grepping and reading files" — which is yet another budget squeeze: turn N round-trips into one wall-clock round, lower latency, same token bill.)&lt;/p&gt;

&lt;p&gt;Pull the threads together and Explore reads like a &lt;em&gt;budgeted exploration worker&lt;/em&gt;: small model, no CLAUDE.md noise, no nested recursion, read-only, parallel-first. It is the most economically tuned piece of the retrieval system, and it's there because once you've decided the model drives retrieval, the next question is &lt;em&gt;which&lt;/em&gt; model, and &lt;em&gt;what&lt;/em&gt; it gets paid to think about.&lt;/p&gt;

&lt;p&gt;The popular telling skips all of this. It says "Anthropic spawns Explore." The source says &lt;em&gt;Anthropic spawns the cheapest possible agent that can do the work&lt;/em&gt;, &lt;em&gt;strips its context to the minimum that lets it function&lt;/em&gt;, and &lt;em&gt;measures whether spawning it at all beats just letting the main agent loop directly&lt;/em&gt;. That last clause is the A/B in the previous section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fork — The Architecture Under Test
&lt;/h2&gt;

&lt;p&gt;Here's the other half of the experiment.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src/tools/AgentTool/forkSubagent.ts&lt;/code&gt; exports &lt;code&gt;isForkSubagentEnabled()&lt;/code&gt;. When that returns true, &lt;code&gt;AgentTool/prompt.ts&lt;/code&gt; rewrites the system prompt to introduce a new operation: &lt;strong&gt;fork&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The fork section, paraphrased from &lt;code&gt;prompt.ts:80-96&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fork yourself (omit &lt;code&gt;subagent_type&lt;/code&gt;) when the intermediate tool output isn't worth keeping in your context. The criterion is qualitative — "will I need this output again" — not task size. Forks are cheap because they share your prompt cache. Don't set &lt;code&gt;model&lt;/code&gt; on a fork — a different model can't reuse the parent's cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't peek.&lt;/strong&gt; The tool result includes an &lt;code&gt;output_file&lt;/code&gt; path — do not Read or tail it unless the user explicitly asks for a progress check. You get a completion notification; trust it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't race.&lt;/strong&gt; After launching, you know nothing about what the fork found. Never fabricate or predict fork results. The notification arrives as a user-role message in a later turn; it is never something you write yourself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read this against Explore and the trade becomes visible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Explore&lt;/th&gt;
&lt;th&gt;Fork&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context isolation&lt;/td&gt;
&lt;td&gt;Fresh subagent, separate context&lt;/td&gt;
&lt;td&gt;Inherits parent context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Haiku (cheap)&lt;/td&gt;
&lt;td&gt;Same as parent (no swap allowed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt cache&lt;/td&gt;
&lt;td&gt;New cache, no parent reuse&lt;/td&gt;
&lt;td&gt;Reuses parent cache (huge savings on the system prompt and prior turns)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLAUDE.md&lt;/td&gt;
&lt;td&gt;Omitted&lt;/td&gt;
&lt;td&gt;Inherited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursion&lt;/td&gt;
&lt;td&gt;Disallowed&lt;/td&gt;
&lt;td&gt;Allowed (forks can fork)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure model&lt;/td&gt;
&lt;td&gt;Subagent returns summary or error&lt;/td&gt;
&lt;td&gt;Notification arrives later as user message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discipline required&lt;/td&gt;
&lt;td&gt;Caller frames the query&lt;/td&gt;
&lt;td&gt;Caller writes a directive prompt; trusts the notification&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither dominates. &lt;strong&gt;Explore&lt;/strong&gt; wins when isolation matters more than cost — the exploration is noisy enough that you don't want any of the tool spew in your main context, and the work is mechanical enough that Haiku can handle it. &lt;strong&gt;Fork&lt;/strong&gt; wins when the work needs the main model's depth and you're willing to pay the cache-reuse savings to get it, accepting that the fork is just a future-you with a directive.&lt;/p&gt;

&lt;p&gt;Anthropic is shipping both, gated, and watching which one wins on internal metrics. The interview answer that says "Claude Code uses Explore for exploration" is &lt;strong&gt;a snapshot of one branch of the experiment&lt;/strong&gt;. The next release cycle may settle it differently.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of fact you can't get from the popular tellings, because they're written from a single observation of behavior. Reading the source gives you the experimental design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version caveat.&lt;/strong&gt; Flag names (&lt;code&gt;BUILTIN_EXPLORE_PLAN_AGENTS&lt;/code&gt;, &lt;code&gt;tengu_amber_stoat&lt;/code&gt;, &lt;code&gt;isForkSubagentEnabled&lt;/code&gt;) and exact gate semantics reflect the snapshot I reviewed and will drift — Anthropic ships fast. The two specific architectures (Explore, Fork) may be renamed, consolidated, or replaced by the time you read this. What's stable is the &lt;em&gt;pattern&lt;/em&gt;: Anthropic running multiple retrieval architectures in parallel, gated, measuring against each other. That pattern outlasts any specific flag name and is what the argument here actually rests on. Verify against the current release before depending on any specific flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  When RAG Still Wins
&lt;/h2&gt;

&lt;p&gt;The cost-curve thesis predicts when RAG crosses over and dominates. Three regimes:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Mega-monorepos where any kind of index is amortized across millions of queries
&lt;/h3&gt;

&lt;p&gt;At Google or Meta or a top-tier hedge fund — codebases large enough that every per-query latency point costs real money across the org — there &lt;em&gt;is&lt;/em&gt; an index. But, importantly, the index they actually run is almost always &lt;strong&gt;symbol-graph&lt;/strong&gt; (Kythe, Glean, Sourcegraph) rather than vector RAG, for the precision reasons in the earlier section. The index is built once, maintained by a dedicated team, amortized across the engineering population's queries forever. Per-query cost goes to single-digit milliseconds; index drift is handled.&lt;/p&gt;

&lt;p&gt;In &lt;em&gt;this&lt;/em&gt; regime, tool-loops are paying the same per-query cost over and over across N engineers, and the math flips toward indexing. Below that scale, the staffing cost of "a dedicated team that owns the code index" is itself a hidden cost that wipes the savings — and the index of choice at smaller scales (per-project Sourcegraph, ctags) is still not vector RAG. Vector RAG specifically tends to win in the next two regimes, not this one.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pure semantic queries where there's no symbol to grep for
&lt;/h3&gt;

&lt;p&gt;"Find the code that handles user-deactivation edge cases when the account is also a billing admin" — there's no specific symbol. There's a conceptual region of the code that you're looking for. A vector search over function-doc embeddings might point you to the right cluster of functions faster than the model would grep.&lt;/p&gt;

&lt;p&gt;Claude Code's answer to this is: spawn Explore, let it iterate. That works, but it isn't free. If your workload is dominated by semantic-cluster queries (auditing, security review, refactor planning), RAG starts to pencil out.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. When LLM context is the scarce resource — cheap, short-context model regime
&lt;/h3&gt;

&lt;p&gt;This is the framing nobody else gives. Cost-curves cut both ways. If your model has a 32k context window and costs $0.50/M tokens, doing six tool round-trips for retrieval is six rounds of context consumption. A one-shot RAG hit lets you spend that context budget on reasoning. RAG dominates when &lt;strong&gt;per-query token cost dominates per-query latency cost&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Anthropic optimized Claude Code for the regime where context is cheap and abundant (Opus 4.7 with a 1M-token window, in some configurations) and per-query latency is what users feel. In a different regime — say, on-device coding agents over a small open-source model — the cost-curve flips and RAG is the right tool. Same first principles, different numerical answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Decision Framework
&lt;/h2&gt;

&lt;p&gt;Match your project to the column. The cost-curve answers the question for you.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Use grep + LLM tool-loop&lt;/th&gt;
&lt;th&gt;Use RAG&lt;/th&gt;
&lt;th&gt;Either / hybrid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Project size: under ~1M lines&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project size: 1M–100M lines&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project size: 100M+ lines (mega-monorepo)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codebase changes daily&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codebase is mostly static (knowledge base, archived)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries are exact-symbol ("find getUserById")&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries are conceptual ("how does auth work")&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model context is cheap and large&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model context is scarce/expensive&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You don't have a team to own a code index&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You have an index team and tooling already (Sourcegraph, Glean, ctags)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The crossover doesn't happen at any one threshold; multiple columns moving in the same direction tips the cost curve. For most non-FAANG projects, the columns sit on the grep side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Companion Question
&lt;/h2&gt;

&lt;p&gt;This post is one half of a two-part argument about Claude Code's retrieval and memory choices. The companion — &lt;strong&gt;"Agent Memory Is a Cache Coherence Problem"&lt;/strong&gt; &lt;em&gt;(publishing 2026-05-28)&lt;/em&gt; — makes the same kind of argument about &lt;em&gt;cross-session&lt;/em&gt; memory: why Claude Code's built-in memory is hand-written Markdown instead of vector-recalled embeddings, even with the world hyping &lt;code&gt;claude-mem&lt;/code&gt; (70k+ GitHub stars as of May 2026) as a drop-in upgrade.&lt;/p&gt;

&lt;p&gt;Read together, the two pieces add up to a coherent design stance. Anthropic's bet, across both axes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fidelity over fuzz.&lt;/strong&gt; Both within-session retrieval (grep) and cross-session memory (CLAUDE.md) are lossless and exact. Both refuse vector approximation as the default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost curves over romance.&lt;/strong&gt; Neither choice is justified by "we trust the model." Both are justified by the math: zero build cost + zero maintain cost beats nonlinear maintain cost for the workloads they target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experimentation in production.&lt;/strong&gt; Both architectures have alternative branches under active flag gating. On the retrieval side: &lt;code&gt;tengu_amber_stoat&lt;/code&gt; for Explore-vs-no-Explore, with Fork as a parallel architecture. On the memory side: &lt;code&gt;tengu_coral_fern&lt;/code&gt;, &lt;code&gt;tengu_herring_clock&lt;/code&gt;, &lt;code&gt;tengu_passport_quail&lt;/code&gt;, &lt;code&gt;tengu_slate_thimble&lt;/code&gt;, plus the build-time gates &lt;code&gt;KAIROS&lt;/code&gt;, &lt;code&gt;TEAMMEM&lt;/code&gt;, and &lt;code&gt;EXTRACT_MEMORIES&lt;/code&gt; — all visible in &lt;code&gt;src/memdir/&lt;/code&gt;. The pattern is the same: ship a default, leave the toggles in, keep measuring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shape of the design is a careful refusal to lock in. The cost curves favor the current choices today. They might not in a year. The system is built to flip.&lt;/p&gt;

&lt;p&gt;That, finally, is the answer the interview question is fishing for. &lt;em&gt;Why not RAG?&lt;/em&gt; Because the cost curves don't justify it for this workload, and Anthropic has the engineering culture to refuse the cargo-cult. &lt;em&gt;Will it always be that way?&lt;/em&gt; No — and the feature flags in the source say so out loud.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Companion piece: **Agent Memory Is a Cache Coherence Problem&lt;/em&gt;* (publishing 2026-05-28)*&lt;br&gt;
&lt;em&gt;Background: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/consistency-scenarios-and-approaches-production/" rel="noopener noreferrer"&gt;Consistency in Distributed Systems: Scenarios, Trade-offs, and What Actually Works&lt;/a&gt;***&lt;br&gt;
&lt;em&gt;For a focused walk through the loop file: *&lt;/em&gt;&lt;a href="https://harrisonsec.com/blog/claude-code-deep-dive-query-loop/" rel="noopener noreferrer"&gt;Claude Code Deep Dive Part 2: The 1,421-Line While Loop&lt;/a&gt;***&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>claudecode</category>
      <category>programming</category>
    </item>
    <item>
      <title>Channels Aren't Message Passing — How Parked Goroutines OOM-Killed a Pod</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Thu, 14 May 2026 05:26:27 +0000</pubDate>
      <link>https://dev.to/harrisonsec/channels-arent-message-passing-how-parked-goroutines-oom-killed-a-pod-4ijf</link>
      <guid>https://dev.to/harrisonsec/channels-arent-message-passing-how-parked-goroutines-oom-killed-a-pod-4ijf</guid>
      <description>&lt;p&gt;It's 3am. The Kafka consumer pod that's been running cleanly for six weeks gets OOM-killed. Kubernetes restarts it. Five minutes later: OOM-killed again. Restart. OOM-killed a third time. By the fourth restart I've shelved the dashboard and started reading &lt;code&gt;runtime/chan.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The code that died fit on one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want to tell you that line is the bug. It isn't. An unbuffered channel will happily backpressure a &lt;em&gt;single&lt;/em&gt; producer — every send rendezvous with a receiver, the producer cannot run ahead. The channel did exactly what it was designed to do.&lt;/p&gt;

&lt;p&gt;What I had built &lt;em&gt;around&lt;/em&gt; it didn't. The Kafka consumer loop wrapped &lt;code&gt;events &amp;lt;- parseEvent(msg)&lt;/code&gt; inside a &lt;code&gt;go func(msg) { ... }(msg)&lt;/code&gt;, spawning a fresh goroutine per inbound message. Every one of those goroutines blocked on send, parked on the channel's &lt;code&gt;sendq&lt;/code&gt; list, and kept its stack and the parsed event alive in memory. The channel was the gravestone. The unbounded &lt;code&gt;go func&lt;/code&gt; fan-out was what filled it.&lt;/p&gt;

&lt;p&gt;This is the story of what a Go channel actually is at the runtime level, why "channels are message passing" is one of the most expensive lies in the Go ecosystem, and why the most common channel bug isn't &lt;em&gt;in&lt;/em&gt; the channel — it's in the code that calls into it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — A Go channel is not a queue and not a message bus. It's a heap-allocated &lt;code&gt;hchan&lt;/code&gt; struct containing a mutex, a ring buffer, and two parked-goroutine lists. The send operation is a &lt;code&gt;memcpy&lt;/code&gt; under a lock, not a transmission. &lt;strong&gt;Channels only deliver backpressure if the producer side is bounded.&lt;/strong&gt; The OOM that started this story came not from &lt;code&gt;make(chan Event)&lt;/code&gt; — that was working as designed — but from an unbounded &lt;code&gt;go func(msg)&lt;/code&gt; fan-out parking thousands of goroutines on &lt;code&gt;sendq&lt;/code&gt;, each retaining a 10KB payload. The fix isn't a buffer size. It's making backpressure part of the producer contract: a single long-lived producer with &lt;code&gt;select&lt;/code&gt;-based backoff, plus a bounded queue as a safety net. The same architectural mistake shows up at every layer where engineers reach for an "in-process queue" — including the inbound queue of your AI agent.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Mental Model That Killed The Pod
&lt;/h2&gt;

&lt;p&gt;Here is what I thought a channel did, and I suspect most Go engineers carry some version of this picture:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A channel is like a Kafka topic in-process. Producers push messages onto it. Consumers pull messages off it. The runtime handles ordering and delivery. It's CSP — Communicating Sequential Processes — Hoare's thing, basically a typed pipe."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every word of that sentence is wrong in a way that matters. There is no topic. Nothing is being pushed anywhere. The runtime is not a broker. The word &lt;em&gt;passing&lt;/em&gt; — borrowed from message-passing concurrency, where independent processes communicate across an isolation boundary — is the most misleading part. In a Go channel, there is no isolation boundary. There is one struct on the heap, and both goroutines reach in and mutate it.&lt;/p&gt;

&lt;p&gt;I held the message-passing model long enough that when the Kafka consumer started ingesting a 12-hour upstream replay at full throttle, I had no instinct that &lt;em&gt;the messages were going somewhere bounded&lt;/em&gt;. They weren't. They were sitting in a ring buffer that I had failed to give a size to.&lt;/p&gt;




&lt;h2&gt;
  
  
  What A Channel Actually Is
&lt;/h2&gt;

&lt;p&gt;Crack open &lt;a href="https://github.com/golang/go/blob/master/src/runtime/chan.go" rel="noopener noreferrer"&gt;runtime/chan.go&lt;/a&gt; in the Go source tree and you'll find this (layout stable since Go 1.7, confirmed against Go 1.21–1.25):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;hchan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;qcount&lt;/span&gt;   &lt;span class="kt"&gt;uint&lt;/span&gt;           &lt;span class="c"&gt;// total data in the queue&lt;/span&gt;
    &lt;span class="n"&gt;dataqsiz&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt;           &lt;span class="c"&gt;// size of the circular queue&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;      &lt;span class="n"&gt;unsafe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pointer&lt;/span&gt; &lt;span class="c"&gt;// points to an array of dataqsiz elements&lt;/span&gt;
    &lt;span class="n"&gt;elemsize&lt;/span&gt; &lt;span class="kt"&gt;uint16&lt;/span&gt;
    &lt;span class="n"&gt;closed&lt;/span&gt;   &lt;span class="kt"&gt;uint32&lt;/span&gt;
    &lt;span class="n"&gt;elemtype&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_type&lt;/span&gt;
    &lt;span class="n"&gt;sendx&lt;/span&gt;    &lt;span class="kt"&gt;uint&lt;/span&gt;           &lt;span class="c"&gt;// send index&lt;/span&gt;
    &lt;span class="n"&gt;recvx&lt;/span&gt;    &lt;span class="kt"&gt;uint&lt;/span&gt;           &lt;span class="c"&gt;// receive index&lt;/span&gt;
    &lt;span class="n"&gt;recvq&lt;/span&gt;    &lt;span class="n"&gt;waitq&lt;/span&gt;          &lt;span class="c"&gt;// list of recv waiters&lt;/span&gt;
    &lt;span class="n"&gt;sendq&lt;/span&gt;    &lt;span class="n"&gt;waitq&lt;/span&gt;          &lt;span class="c"&gt;// list of send waiters&lt;/span&gt;
    &lt;span class="n"&gt;lock&lt;/span&gt;     &lt;span class="n"&gt;mutex&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the channel. A struct with a mutex, a pointer to a circular byte array, two indices to track read/write positions in the ring, and two intrusive linked lists holding parked goroutines that are waiting to send or receive.&lt;/p&gt;

&lt;p&gt;When you write &lt;code&gt;ch &amp;lt;- value&lt;/code&gt;, the runtime calls &lt;a href="https://github.com/golang/go/blob/master/src/runtime/chan.go#L160" rel="noopener noreferrer"&gt;&lt;code&gt;chansend&lt;/code&gt;&lt;/a&gt;, which does roughly this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Take the lock&lt;/strong&gt; (&lt;code&gt;lock(&amp;amp;c.lock)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check &lt;code&gt;recvq&lt;/code&gt;&lt;/strong&gt; — is there a goroutine already parked waiting to receive? If yes, copy &lt;code&gt;value&lt;/code&gt; &lt;em&gt;directly&lt;/em&gt; from the sender's stack into the receiver's stack via &lt;code&gt;sendDirect&lt;/code&gt;, mark the receiver runnable with &lt;code&gt;goready&lt;/code&gt;, release the lock, return. No buffer involved — when a receiver is already waiting, send can hand off directly without ever touching the ring buffer. (In normal operation a buffered channel can't simultaneously have queued data AND parked receivers; if &lt;code&gt;recvq&lt;/code&gt; has a waiter, the buffer is empty.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Otherwise, check buffer space&lt;/strong&gt; — if &lt;code&gt;qcount &amp;lt; dataqsiz&lt;/code&gt;, copy &lt;code&gt;value&lt;/code&gt; into &lt;code&gt;buf[sendx]&lt;/code&gt;, advance &lt;code&gt;sendx&lt;/code&gt;, increment &lt;code&gt;qcount&lt;/code&gt;, release the lock, return.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Otherwise, park the sender&lt;/strong&gt; — append the sender's goroutine to &lt;code&gt;sendq&lt;/code&gt;, release the lock, and call &lt;code&gt;gopark&lt;/code&gt; to suspend execution until a receiver wakes it up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Receive is the mirror image, calling &lt;code&gt;chanrecv&lt;/code&gt; with &lt;code&gt;sendq&lt;/code&gt; and &lt;code&gt;recvq&lt;/code&gt; swapped.&lt;/p&gt;

&lt;p&gt;Here is the shape of it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQKICAgIHN1YmdyYXBoIFNlbmRlciBbU2VuZGVyIGdvcm91dGluZV0KICAgICAgICBTMVsiY2ggJmx0Oy0gdmFsdWUiXQogICAgZW5kCgogICAgc3ViZ3JhcGggQ2hhbm5lbCBbaGNoYW4gc3RydWN0IG9uIGhlYXBdCiAgICAgICAgTFttdXRleCBsb2NrXQogICAgICAgIEJbInJpbmcgYnVmZmVyPGJyLz5kYXRhcXNpeiBzbG90cyJdCiAgICAgICAgUlFbcmVjdnE6IHBhcmtlZCByZWNlaXZlcnNdCiAgICAgICAgU1Fbc2VuZHE6IHBhcmtlZCBzZW5kZXJzXQogICAgZW5kCgogICAgc3ViZ3JhcGggUmVjZWl2ZXIgW1JlY2VpdmVyIGdvcm91dGluZV0KICAgICAgICBSMVsidiA6PSAmbHQ7LWNoIl0KICAgIGVuZAoKICAgIFMxIC0tPnwiMS4gYWNxdWlyZSBsb2NrInwgTAogICAgTCAtLT58IjIuIHJlY3ZxIGVtcHR5PyJ8IEIKICAgIEwgLS0-fCIyLiByZWN2cSBoYXMgd2FpdGVyInwgUlEKICAgIFJRIC0tPnwiZGlyZWN0IGNvcHksIG5vIGJ1ZmZlciJ8IFIxCiAgICBCIC0tPnwiY29weSB0byBidWYgaWYgc3BhY2UifCBSMQogICAgTCAtLT58ImJ1ZmZlciBmdWxsLCBwYXJrIHNlbmRlciJ8IFNR" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEQKICAgIHN1YmdyYXBoIFNlbmRlciBbU2VuZGVyIGdvcm91dGluZV0KICAgICAgICBTMVsiY2ggJmx0Oy0gdmFsdWUiXQogICAgZW5kCgogICAgc3ViZ3JhcGggQ2hhbm5lbCBbaGNoYW4gc3RydWN0IG9uIGhlYXBdCiAgICAgICAgTFttdXRleCBsb2NrXQogICAgICAgIEJbInJpbmcgYnVmZmVyPGJyLz5kYXRhcXNpeiBzbG90cyJdCiAgICAgICAgUlFbcmVjdnE6IHBhcmtlZCByZWNlaXZlcnNdCiAgICAgICAgU1Fbc2VuZHE6IHBhcmtlZCBzZW5kZXJzXQogICAgZW5kCgogICAgc3ViZ3JhcGggUmVjZWl2ZXIgW1JlY2VpdmVyIGdvcm91dGluZV0KICAgICAgICBSMVsidiA6PSAmbHQ7LWNoIl0KICAgIGVuZAoKICAgIFMxIC0tPnwiMS4gYWNxdWlyZSBsb2NrInwgTAogICAgTCAtLT58IjIuIHJlY3ZxIGVtcHR5PyJ8IEIKICAgIEwgLS0-fCIyLiByZWN2cSBoYXMgd2FpdGVyInwgUlEKICAgIFJRIC0tPnwiZGlyZWN0IGNvcHksIG5vIGJ1ZmZlciJ8IFIxCiAgICBCIC0tPnwiY29weSB0byBidWYgaWYgc3BhY2UifCBSMQogICAgTCAtLT58ImJ1ZmZlciBmdWxsLCBwYXJrIHNlbmRlciJ8IFNR" alt="graph TD" width="792" height="628"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things are worth burning into memory:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One — there is no transport.&lt;/strong&gt; The "message" never leaves the heap. Sender writes bytes; receiver reads bytes; the lock arbitrates. This is shared-memory synchronization with the &lt;em&gt;appearance&lt;/em&gt; of message passing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two — the buffer is just a ring of typed slots.&lt;/strong&gt; &lt;code&gt;dataqsiz&lt;/code&gt; is set exactly once, at &lt;code&gt;make(chan T, N)&lt;/code&gt; time. If you write &lt;code&gt;make(chan T)&lt;/code&gt;, &lt;code&gt;dataqsiz&lt;/code&gt; is zero and there is no buffer at all — every send must rendezvous with a receiver or park.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three — &lt;code&gt;sendq&lt;/code&gt; is unbounded.&lt;/strong&gt; This is the part nobody talks about. The ring buffer has a fixed size. The list of &lt;em&gt;parked senders waiting to write into the ring buffer&lt;/em&gt; does not. If a thousand goroutines all hit a full channel, the runtime parks all thousand of them on &lt;code&gt;sendq&lt;/code&gt; and each one keeps its stack and any data it was about to send alive in memory.&lt;/p&gt;

&lt;p&gt;That third point is what made the OOM I had a different shape from the one I was about to describe.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Incident, Mechanism By Mechanism
&lt;/h2&gt;

&lt;p&gt;The pod that died had a goroutine topology that looked like this — and the bug is &lt;em&gt;not&lt;/em&gt; the &lt;code&gt;make(chan Event)&lt;/code&gt; line. Watch the outer loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Consumer — slow.&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&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;ev&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// ~3ms per event&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;

&lt;span class="c"&gt;// THE ACTUAL BUG: outer loop spawns a fresh goroutine per inbound message.&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;kafkaConsumer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Messages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="n"&gt;kafka&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;parseEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// every blocked send parks on sendq&lt;/span&gt;
    &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you replace the inner &lt;code&gt;go func(msg) { ... }(msg)&lt;/code&gt; with a direct &lt;code&gt;events &amp;lt;- parseEvent(msg)&lt;/code&gt;, the outer loop &lt;em&gt;itself&lt;/em&gt; becomes the producer, and the unbuffered channel correctly backpressures it — the loop simply doesn't advance until the consumer is ready. No OOM.&lt;/p&gt;

&lt;p&gt;But because each message is dispatched to a fresh helper goroutine, the outer loop never blocks. It keeps spawning. Each helper goroutine reaches the send, finds no waiting receiver, and parks on &lt;code&gt;sendq&lt;/code&gt;. Now &lt;code&gt;sendq&lt;/code&gt; is the unbounded thing. Here is what actually happened, in order:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Sustained baseline: rendezvous works
&lt;/h3&gt;

&lt;p&gt;At 1K msg/sec inbound and ~3ms per &lt;code&gt;process&lt;/code&gt; call (~333/sec consumer throughput), the consumer is already behind by 3x at steady state. For weeks this didn't OOM because the Kafka client's own internal buffer absorbed the gap, and lag built up on the broker side — visible in Grafana, ignored by me.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Replay: the producer detaches from the consumer's pace
&lt;/h3&gt;

&lt;p&gt;When upstream re-emitted 12 hours of events, the Kafka client's internal pre-fetch buffer filled to capacity (default &lt;code&gt;fetch.message.max.bytes&lt;/code&gt; × partition count = several hundred MB) and started backing up &lt;em&gt;Kafka-side&lt;/em&gt; without applying backpressure to the consumer goroutine, because the client library was configured with a large internal queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The actual heap growth: parked sender goroutines
&lt;/h3&gt;

&lt;p&gt;Each call to &lt;code&gt;events &amp;lt;- parseEvent(msg)&lt;/code&gt; on the unbuffered channel would either rendezvous (rare during replay) or park. When it parked, the sender goroutine held:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its own stack (~8KB minimum, grew under load)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Event&lt;/code&gt; value it was about to send (~10KB per event with strings, headers, payload)&lt;/li&gt;
&lt;li&gt;A reference into the Kafka message it was parsing (another ~10KB)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multiply by the number of in-flight parsing goroutines — which kept being spawned by an outer loop that didn't apply backpressure to itself — and you arrive at the 12GB heap. The channel's &lt;code&gt;sendq&lt;/code&gt; was the proximate memory sink, not the buffer (which was zero-sized).&lt;/p&gt;

&lt;p&gt;The goroutine lifecycle for each parsing goroutine looked like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc3RhdGVEaWFncmFtLXYyCiAgICBbKl0gLS0-IFJ1bm5pbmc6IGdvIGZ1bmMoKQogICAgUnVubmluZyAtLT4gUGFya2VkX29uX3NlbmRxOiBjaCA8LSB2YWx1ZSAobm8gcmVjZWl2ZXIpCiAgICBQYXJrZWRfb25fc2VuZHEgLS0-IFJ1bm5hYmxlOiByZWNlaXZlciB3YWtlcyBtZQogICAgUnVubmFibGUgLS0-IFJ1bm5pbmc6IHNjaGVkdWxlciBwaWNrcyBtZQogICAgUnVubmluZyAtLT4gWypdOiBmdW5jdGlvbiByZXR1cm5zCgogICAgbm90ZSByaWdodCBvZiBQYXJrZWRfb25fc2VuZHEKICAgICAgICBTdGFjayByZXRhaW5lZC4KICAgICAgICBFdmVudCBwYXlsb2FkIHJldGFpbmVkLgogICAgICAgIEthZmthIG1zZyByZWZlcmVuY2UgcmV0YWluZWQuCiAgICAgICAgc2VuZHEgaGFzIE5PIHNpemUgYm91bmQuCiAgICBlbmQgbm90ZQ%3D%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc3RhdGVEaWFncmFtLXYyCiAgICBbKl0gLS0-IFJ1bm5pbmc6IGdvIGZ1bmMoKQogICAgUnVubmluZyAtLT4gUGFya2VkX29uX3NlbmRxOiBjaCA8LSB2YWx1ZSAobm8gcmVjZWl2ZXIpCiAgICBQYXJrZWRfb25fc2VuZHEgLS0-IFJ1bm5hYmxlOiByZWNlaXZlciB3YWtlcyBtZQogICAgUnVubmFibGUgLS0-IFJ1bm5pbmc6IHNjaGVkdWxlciBwaWNrcyBtZQogICAgUnVubmluZyAtLT4gWypdOiBmdW5jdGlvbiByZXR1cm5zCgogICAgbm90ZSByaWdodCBvZiBQYXJrZWRfb25fc2VuZHEKICAgICAgICBTdGFjayByZXRhaW5lZC4KICAgICAgICBFdmVudCBwYXlsb2FkIHJldGFpbmVkLgogICAgICAgIEthZmthIG1zZyByZWZlcmVuY2UgcmV0YWluZWQuCiAgICAgICAgc2VuZHEgaGFzIE5PIHNpemUgYm91bmQuCiAgICBlbmQgbm90ZQ%3D%3D" alt="stateDiagram-v2" width="612" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every goroutine sitting in &lt;code&gt;Parked_on_sendq&lt;/code&gt; is reachable (it's on the runtime's wait queue, which is rooted in the &lt;code&gt;hchan&lt;/code&gt; struct, which is rooted by both the producer and consumer goroutines). Reachable means non-collectible. The longer the consumer falls behind, the longer the queue grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. GC can't help
&lt;/h3&gt;

&lt;p&gt;Go's GC can only reclaim unreachable memory. Every parked goroutine on &lt;code&gt;sendq&lt;/code&gt; is reachable (it's on the runtime's scheduler queue). Every &lt;code&gt;Event&lt;/code&gt; it's holding is reachable. The GC ran, found nothing to free, and the heap continued growing until the kernel OOM-killer fired.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The cgroup hammer drops
&lt;/h3&gt;

&lt;p&gt;cgroup memory limit was 4GB. Heap crossed 4GB. OOM kill. Kubernetes restarted the pod. The replay was still in progress on the broker side, so the same sequence ran again. And again.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this looks like in pprof
&lt;/h3&gt;

&lt;p&gt;You don't have to take my word for the mechanism — it reproduces in under a minute. I built a minimal demo at &lt;a href="https://github.com/harrison001/channels-oom-demo" rel="noopener noreferrer"&gt;&lt;code&gt;harrison001/channels-oom-demo&lt;/code&gt;&lt;/a&gt; (&lt;a href="https://github.com/harrison001/channels-oom-demo/blob/main/cmd/bug/main.go" rel="noopener noreferrer"&gt;&lt;code&gt;cmd/bug&lt;/code&gt;&lt;/a&gt;) that runs the same workload shape on a laptop. The output of the bug version over 22 seconds, captured with &lt;code&gt;runtime.NumGoroutine()&lt;/code&gt; and &lt;code&gt;runtime.MemStats.HeapAlloc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t=   1s  goroutines=   497  heap_alloc=     5 MB
t=   5s  goroutines=  2462  heap_alloc=    28 MB
t=  10s  goroutines=  4915  heap_alloc=    61 MB
t=  15s  goroutines=  7369  heap_alloc=    89 MB
t=  20s  goroutines=  9828  heap_alloc=   109 MB
t=  22s  goroutines= 10813  heap_alloc=   125 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Goroutine count grows at almost exactly 1 per millisecond (the spawn rate). Heap grows at ~5MB/sec, dominated by the 10KB Event payload each parked goroutine is holding. Extrapolate to a 12-hour replay at production volume and you arrive at the original 12GB OOM.&lt;/p&gt;

&lt;p&gt;For comparison, the fix version (&lt;a href="https://github.com/harrison001/channels-oom-demo/blob/main/cmd/fix/main.go" rel="noopener noreferrer"&gt;&lt;code&gt;cmd/fix&lt;/code&gt;&lt;/a&gt;) on the same workload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t=   1s  goroutines=     3  heap_alloc=     3 MB  chan_len= 256
t=  10s  goroutines=     3  heap_alloc=     4 MB  chan_len= 256
t=  20s  goroutines=     3  heap_alloc=     5 MB  chan_len= 256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three goroutines (producer, consumer, pprof listener). Heap flat at 4-5 MB. Channel pinned at its 256-slot bound, meaning the producer is constantly blocked on send and applying backpressure upstream — exactly what we want.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix, And Why It Works
&lt;/h2&gt;

&lt;p&gt;The visible code change was one parameter. The real fix was making backpressure part of the producer contract — two changes, working together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// (1) bounded queue as safety net&lt;/span&gt;

&lt;span class="c"&gt;// (2) single long-lived producer goroutine with select-based backoff —&lt;/span&gt;
&lt;span class="c"&gt;// NO outer loop spawning fresh goroutines per message.&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&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;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;kafkaConsumer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Messages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;parseEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="c"&gt;// sent — loop continues at consumer speed when channel fills&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key word in change (2) is &lt;strong&gt;single&lt;/strong&gt;. There is exactly one goroutine reading from Kafka and writing to the channel. When the channel fills, that goroutine blocks on send; the &lt;code&gt;for msg := range&lt;/code&gt; loop stops calling &lt;code&gt;Poll()&lt;/code&gt;; the Kafka client's internal pre-fetch queue stops draining; consumer lag accumulates broker-side; the broker simply retains messages until we come back. No &lt;code&gt;go func(msg)&lt;/code&gt; helpers. Nothing piling up on &lt;code&gt;sendq&lt;/code&gt;. Memory stays bounded because the &lt;em&gt;producer&lt;/em&gt; is bounded — the buffer is only a safety net to absorb short bursts.&lt;/p&gt;

&lt;p&gt;What this changes, mechanically:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before (unbounded &lt;code&gt;go func&lt;/code&gt; fan-out + &lt;code&gt;make(chan Event)&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;After (single producer + &lt;code&gt;make(chan Event, 256)&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One goroutine per inbound message&lt;/td&gt;
&lt;td&gt;One long-lived producer goroutine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sendq&lt;/code&gt; grows unboundedly with parked helpers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sendq&lt;/code&gt; empty by construction; producer is sole sender&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No signal to upstream — outer loop never blocks&lt;/td&gt;
&lt;td&gt;Producer blocks on send; outer loop runs at consumer speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka client keeps pre-fetching, lag invisible&lt;/td&gt;
&lt;td&gt;Kafka client's internal queue fills, consumer stops polling, broker-side lag accumulates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OOM&lt;/td&gt;
&lt;td&gt;Bounded heap, bounded latency, Kafka rebalances cleanly when behind&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A bounded channel buffer alone does not prevent this OOM. If you applied change (1) without change (2), you'd merely increase the OOM-killing rate — the outer &lt;code&gt;go func(msg)&lt;/code&gt; fan-out would keep spawning, the buffer would fill in milliseconds, helpers would pile up on &lt;code&gt;sendq&lt;/code&gt; exactly as before. Backpressure is not a property of any one component — it is a property of the entire chain having no unbounded buffer (and no unbounded fan-out) anywhere in it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggTFIKICAgIEFbS2Fma2EgYnJva2VyXSAtLT58ZmV0Y2h8IEJbS2Fma2EgY2xpZW50PGJyLz5wcmUtZmV0Y2ggYnVmZmVyXQogICAgQiAtLT4gQ1tQcm9kdWNlcjxici8-Z29yb3V0aW5lXQogICAgQyAtLT58c2VsZWN0fCBEWyJldmVudHM8YnIvPmNoYW4gVCwgMjU2Il0KICAgIEQgLS0-IEVbQ29uc3VtZXI8YnIvPmdvcm91dGluZV0KICAgIEUgLS0-IEZbKERhdGFiYXNlKV0KCiAgICBGIC0uIHNsb3cgLi0-IEUKICAgIEUgLS4gc2xvdyBkcmFpbiAuLT4gRAogICAgRCAtLiBmdWxsIC4tPiBDCiAgICBDIC0uIGJsb2NrcyBvbiBzZW5kIC4tPiBCCiAgICBCIC0uIHF1ZXVlIGZpbGxzLCBmZXRjaCBzbG93cyAuLT4gQQogICAgQSAtLiBicm9rZXIgcmV0YWlucyBtc2dzPGJyLz5jb25zdW1lciBsYWcgZ3Jvd3MgLi0-IEEKCiAgICBjbGFzc0RlZiBib3VuZCBmaWxsOiNjZmUsc3Ryb2tlOiMwODAKICAgIGNsYXNzIEQgYm91bmQ%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggTFIKICAgIEFbS2Fma2EgYnJva2VyXSAtLT58ZmV0Y2h8IEJbS2Fma2EgY2xpZW50PGJyLz5wcmUtZmV0Y2ggYnVmZmVyXQogICAgQiAtLT4gQ1tQcm9kdWNlcjxici8-Z29yb3V0aW5lXQogICAgQyAtLT58c2VsZWN0fCBEWyJldmVudHM8YnIvPmNoYW4gVCwgMjU2Il0KICAgIEQgLS0-IEVbQ29uc3VtZXI8YnIvPmdvcm91dGluZV0KICAgIEUgLS0-IEZbKERhdGFiYXNlKV0KCiAgICBGIC0uIHNsb3cgLi0-IEUKICAgIEUgLS4gc2xvdyBkcmFpbiAuLT4gRAogICAgRCAtLiBmdWxsIC4tPiBDCiAgICBDIC0uIGJsb2NrcyBvbiBzZW5kIC4tPiBCCiAgICBCIC0uIHF1ZXVlIGZpbGxzLCBmZXRjaCBzbG93cyAuLT4gQQogICAgQSAtLiBicm9rZXIgcmV0YWlucyBtc2dzPGJyLz5jb25zdW1lciBsYWcgZ3Jvd3MgLi0-IEEKCiAgICBjbGFzc0RlZiBib3VuZCBmaWxsOiNjZmUsc3Ryb2tlOiMwODAKICAgIGNsYXNzIEQgYm91bmQ%3D" alt="graph LR" width="1521" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every link in this chain is bounded — the database has connection pool limits, the consumer is rate-limited by &lt;code&gt;process()&lt;/code&gt; latency, the channel buffer is 256, the Kafka client's internal queue has a configured max, and the broker simply retains messages on disk when its consumer falls behind. When ANY downstream link slows, the pressure propagates back up by the consumer ceasing to pull; the broker doesn't need to be told anything. The whole system runs at the rate of its slowest component.&lt;/p&gt;

&lt;p&gt;If any link in that chain has an unbounded buffer, the chain has no backpressure. That link will absorb the load until it OOMs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bounded Buffers Are Not About Channels
&lt;/h2&gt;

&lt;p&gt;The lesson is not "use buffered channels." The lesson is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Any in-process queue without a capacity bound is a latent OOM.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This applies identically across runtimes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;The footgun&lt;/th&gt;
&lt;th&gt;The fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Unbounded goroutine fan-out parked on sends (&lt;code&gt;go func(msg) { ch &amp;lt;- ... }(msg)&lt;/code&gt;); oversized buffered channels&lt;/td&gt;
&lt;td&gt;Single long-lived producer + &lt;code&gt;select&lt;/code&gt; + bounded buffer as safety net&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust (Tokio)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mpsc::unbounded_channel()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mpsc::channel(N)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python (asyncio)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;asyncio.Queue()&lt;/code&gt; with no &lt;code&gt;maxsize&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;asyncio.Queue(maxsize=N)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;Unbounded array of in-flight Promises&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;p-limit&lt;/code&gt;, &lt;code&gt;Sema&lt;/code&gt;, or explicit pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Erlang/Elixir&lt;/td&gt;
&lt;td&gt;Process mailbox grows unboundedly when selective receive can't keep up&lt;/td&gt;
&lt;td&gt;Demand-driven flow control: &lt;a href="https://hexdocs.pm/gen_stage/GenStage.html" rel="noopener noreferrer"&gt;&lt;code&gt;GenStage&lt;/code&gt;&lt;/a&gt; / &lt;code&gt;Flow&lt;/code&gt; for pipelines, or explicit ack-based protocols in &lt;code&gt;gen_statem&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of these reaches for the same shape — an in-process queue — and every one of them OOMs the same way when the shape is unbounded.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Channels Are The Right Tool
&lt;/h2&gt;

&lt;p&gt;I want to be careful not to overcorrect. Channels are not a mistake. They are an excellent primitive used incorrectly. Cases where reaching for a channel is the right call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cancellation signaling&lt;/strong&gt; — &lt;code&gt;context.Done()&lt;/code&gt; is a &lt;code&gt;&amp;lt;-chan struct{}&lt;/code&gt;. This is canonical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out work distribution with a worker pool&lt;/strong&gt; — a bounded channel feeding N worker goroutines is a clean semaphore. Buffer size = pool size or small multiple of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Producer-consumer with a known throughput ratio&lt;/strong&gt; — yes, with a bounded buffer sized to the latency budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error aggregation from concurrent goroutines&lt;/strong&gt; — small buffered channel, drain on goroutine completion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handoff between pipeline stages&lt;/strong&gt; — bounded, with explicit close semantics on the upstream stage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cases where reaching for a channel is the wrong call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-process messaging&lt;/strong&gt; — use a real broker (NATS, Kafka, Redis Streams). Channels do not survive a pod restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence&lt;/strong&gt; — channels are stack-local-ish. If your pod dies, the in-flight data is gone. If you need "at least once" across restarts, you need a real queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bursty load with unknown shape&lt;/strong&gt; — if you cannot put a meaningful upper bound on the buffer, you have not understood the load. Adding a channel does not give you understanding; it postpones the OOM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything that wants to be a message bus&lt;/strong&gt; — that's not a channel. That's a message bus. They are different categories of system.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Same Bug, Different Layer: AI Agent Inbound Queues
&lt;/h2&gt;

&lt;p&gt;The reason this post lives in the SecurityLab track and not just "Go tips" is that the exact same mistake is now happening, at scale, in LLM agent infrastructure. I've seen the pattern repeatedly in recent AI backends — same architectural shape, different runtime.&lt;/p&gt;

&lt;p&gt;The pattern: an agent backend exposes an HTTP endpoint. Each inbound request is dispatched to a worker pool via an in-process queue.&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;# The bug, in a different language
&lt;/span&gt;&lt;span class="n"&gt;request_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# unbounded
&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;http_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# never blocks
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;worker&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;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;llm_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 8 seconds, sometimes 30
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Steady state is fine: requests arrive faster than they're processed, queue grows slowly, latency creeps up, nobody notices because the HTTP layer keeps returning 200.&lt;/p&gt;

&lt;p&gt;Then a launch happens. Or a viral tweet. Or a marketing email goes out. Inbound rate spikes 50x for 20 minutes. The queue accepts everything (it's unbounded). The worker pool can't keep up — LLM calls are inelastic, you can't parallelize past your token-per-minute quota. The queue grows to 200K items. Each item holds a request payload (~50KB with conversation history) and a future. 10GB of heap. OOM. Pod restart. All 200K requests lost. Users see 500s instead of the explicit "rate-limited, try again in 30s" they would have seen with proper backpressure.&lt;/p&gt;

&lt;p&gt;The fix is identical to the Go fix:&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;request_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;http_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;request_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_nowait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueueFull&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&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;Retry-After&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;30&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;queued&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;503 is a feature. It is the system telling the client &lt;em&gt;we're at capacity, retry in 30 seconds&lt;/em&gt;. It is honest. It is bounded. It is the difference between a system that degrades gracefully and one that dies silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproducing This Yourself
&lt;/h2&gt;

&lt;p&gt;The numbers in this post come from a minimal Go program that fits in under 100 lines per command. The repo lives at:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/harrison001/channels-oom-demo" rel="noopener noreferrer"&gt;github.com/harrison001/channels-oom-demo&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/harrison001/channels-oom-demo.git
&lt;span class="nb"&gt;cd &lt;/span&gt;channels-oom-demo

&lt;span class="c"&gt;# Watch goroutine count + heap climb every second&lt;/span&gt;
go run ./cmd/bug

&lt;span class="c"&gt;# Switch to the fix — flat at 3 goroutines, 5 MB heap&lt;/span&gt;
go run ./cmd/fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each program exposes pprof on &lt;code&gt;localhost:6060&lt;/code&gt;. While the bug version is running:&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;# Confirm 10K+ goroutines parked on chansend → runtime_chanrecv1&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'http://localhost:6060/debug/pprof/goroutine?debug=1'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# Confirm the heap is dominated by Event payloads, not the channel itself&lt;/span&gt;
go tool pprof &lt;span class="nt"&gt;-text&lt;/span&gt; http://localhost:6060/debug/pprof/heap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug demo has a hard cap at 20,000 goroutines so it won't actually OOM your laptop. Remove the cap if you want to see the kernel finish the job.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Wish I'd Known
&lt;/h2&gt;

&lt;p&gt;If I could send one note back to myself eighteen months before the OOM:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When you reach for an in-process queue, you are choosing a backpressure boundary. The buffer size is not a performance tuning knob. It is a contract: &lt;em&gt;under sustained load greater than my consumer's throughput, this is how much memory I am willing to lose before I tell the producer to stop.&lt;/em&gt; If you don't pick a number, the runtime picks one for you, and the number is &lt;em&gt;whatever fits in RAM right before the kernel kills the process.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Channels in Go look like message-passing because the syntax was deliberately borrowed from CSP, a model where independent processes communicate by passing values across an isolation boundary. In Go there is no isolation boundary. The channel is a struct in shared memory, the goroutines are coroutines on the same scheduler, and the entire setup is synchronization plumbing in CSP clothing.&lt;/p&gt;

&lt;p&gt;Once you see the &lt;code&gt;hchan&lt;/code&gt; struct, you can't un-see it. Every channel decision after that is a synchronization decision, not a transport decision. And synchronization decisions always have a capacity bound — you just have to choose whether to pick it explicitly or have the OOM-killer pick it for you.&lt;/p&gt;




&lt;h3&gt;
  
  
  Keep going
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code&lt;/strong&gt;: &lt;a href="https://github.com/harrison001/channels-oom-demo" rel="noopener noreferrer"&gt;&lt;code&gt;harrison001/channels-oom-demo&lt;/code&gt;&lt;/a&gt; — reproduce both versions, capture your own pprof&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next piece&lt;/strong&gt;: &lt;em&gt;Goroutines Are Cheap — Until Backpressure Is Missing&lt;/em&gt; — coming next. The producer side of the same mistake: why "just spawn a goroutine" is the second half of the bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscribe&lt;/strong&gt;: I write one of these monthly on runtime mechanics, distributed systems postmortems, and the security implications of getting them wrong. &lt;a href="https://buttondown.com/harrisonsec" rel="noopener noreferrer"&gt;Newsletter&lt;/a&gt; · &lt;a href="https://harrisonsec.com/track/securitylab/" rel="noopener noreferrer"&gt;SecurityLab track&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you've hit this bug — or its cousin in a different runtime — I'd genuinely like to hear about it. The Erlang and Node.js shapes especially: I have hunches but not enough scars. Reply to the newsletter or open an issue on the demo repo.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>go</category>
      <category>concurrency</category>
      <category>performance</category>
      <category>backend</category>
    </item>
    <item>
      <title>How I Improved an AI Agent from 40% to 60% — With A/B Test Data</title>
      <dc:creator>Harrison Guo</dc:creator>
      <pubDate>Tue, 12 May 2026 15:49:19 +0000</pubDate>
      <link>https://dev.to/harrisonsec/how-i-improved-an-ai-agent-from-40-to-60-with-ab-test-data-4f2i</link>
      <guid>https://dev.to/harrisonsec/how-i-improved-an-ai-agent-from-40-to-60-with-ab-test-data-4f2i</guid>
      <description>&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I was optimizing an AI agent for a production system — a creator agent that handles user requests like "make this character fiercer" or "rename this entity." The agent runs a 5-layer pipeline: Perceive → Cognate → Decide → Act → Express, with real LLM calls at each step.&lt;/p&gt;

&lt;p&gt;Quality was bad. Not "it doesn't work" bad — "it works 40% of the time" bad. The remaining 60% were wrong entity targeting, infinite reasoning loops, and silent failures.&lt;/p&gt;

&lt;p&gt;I ran 5 standardized test cases, each repeated 5 times (LLMs are non-deterministic), measuring pass rate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;QL-001&lt;/td&gt;
&lt;td&gt;Create 4 entities + 1 relationship in one message&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QL-002&lt;/td&gt;
&lt;td&gt;Classify user intent correctly&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QL-003&lt;/td&gt;
&lt;td&gt;Update the right entity in a world with 6 characters + 4 locations&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QL-004&lt;/td&gt;
&lt;td&gt;Maintain context across long conversation&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QL-005&lt;/td&gt;
&lt;td&gt;Simple rename ("Ember" → "Infernia")&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Overall: 40% pass rate.&lt;/strong&gt; The model (equivalent to GPT-4 class) was plenty capable. Something else was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Diagnosis: Context Was the Problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  QL-003: Why the Agent Confused Entities (40% → 80%)
&lt;/h3&gt;

&lt;p&gt;The user says: "Make Ember more fierce and give her fire breath."&lt;/p&gt;

&lt;p&gt;The world has 10 entities: 6 characters (Ember, Luna, Grak, Roland, Mira, Pip) and 4 locations. The agent's &lt;code&gt;BuildChatCompletionMessages&lt;/code&gt; function dumped ALL entity data into the prompt — every character's backstory, every location's description.&lt;/p&gt;

&lt;p&gt;The LLM had to find Ember in a wall of irrelevant text. Sometimes it picked Luna. Sometimes it referenced the wrong character's traits. Not because the model was stupid — because the context was noisy.&lt;/p&gt;

&lt;h3&gt;
  
  
  QL-005: Why Simple Rename Failed (20% → 80%)
&lt;/h3&gt;

&lt;p&gt;"Rename Ember to Infernia." One entity, one operation. Should be trivial.&lt;/p&gt;

&lt;p&gt;Two problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No round limit — the agent sometimes looped 15+ times on a rename, reasoning tools firing endlessly&lt;/li&gt;
&lt;li&gt;When a tool failed, the LLM got: &lt;code&gt;{"error": true, "message": "This tool is temporarily unavailable."}&lt;/code&gt; — no context on what to do next&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model gave up or produced responses that didn't contain "Infernia."&lt;/p&gt;

&lt;h3&gt;
  
  
  QL-001: Why Multi-Step Creation Was Impossible (0% → 0%)
&lt;/h3&gt;

&lt;p&gt;"Create a dragon named Ember who lives in Crystal Caves. Ember has a rivalry with Sir Roland who guards the village gate."&lt;/p&gt;

&lt;p&gt;This requires creating 4 entities + 1 relationship. The 5-layer pipeline processes entities sequentially, each in isolation. The relationship creation doesn't know the knight was just created — there's no shared state between action steps.&lt;/p&gt;

&lt;p&gt;Both baseline and improved scored 0%. This is an architectural problem, not a context problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fixes: 8 Changes, 7 Pure Code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix 1: PlanExecution (the only LLM call)
&lt;/h3&gt;

&lt;p&gt;One API call before the main loop. The LLM generates a plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Goal: Update Ember's properties
Steps: 1. Identify Ember entity  2. Apply personality changes
Tools needed: updateCharacter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This plan gets injected into the cognition layer's context. The intent classifier now sees a roadmap, not just raw entity data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; ~$0.003 per request, 3-5s latency. The only fix that uses an LLM call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 2: PrioritizeContext (pure code)
&lt;/h3&gt;

&lt;p&gt;Sort context items by salience score. Higher-relevance items go first. Low-relevance items dropped when the token budget is exceeded.&lt;/p&gt;

&lt;p&gt;When the user says "Make Ember fiercer," Ember's data gets priority. Luna's backstory gets dropped. The LLM sees signal, not noise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&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;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Salience&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Salience&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;tokenBudget&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Zero. Pure sort + filter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 3: CompressContext (pure code)
&lt;/h3&gt;

&lt;p&gt;Old conversation rounds get summarized extractively — find tool names, find CONCLUSION markers, truncate the rest. No LLM needed for this level of compression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Zero. String operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 4: Preserve Conclusions (pure code)
&lt;/h3&gt;

&lt;p&gt;When reasoning text is truncated at 4,000 characters, the truncation used to cut wherever it landed. If the LLM decided "I need to rename Ember to Infernia" in round 1 but that conclusion was at character 4,100, round 2 forgot the decision.&lt;/p&gt;

&lt;p&gt;Fix: &lt;code&gt;truncateReasoningPreservingConclusions()&lt;/code&gt; finds CONCLUSION/DECISION markers and keeps them even when truncating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Zero. String search.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 5: Max Rounds Cap (pure code)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;DefaultMaxRounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;roundCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DefaultMaxRounds&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Previously unlimited. The agent sometimes looped 15+ rounds on a trivial task. Now it stops at 10 and produces its best result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Zero. One if-statement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 6: Structured Tool Errors (pure code)
&lt;/h3&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"updateCharacter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This tool is temporarily unavailable."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"updateCharacter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This tool is temporarily unavailable."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"error_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;retryable: true&lt;/code&gt;, the LLM knows to try again instead of giving up. With &lt;code&gt;error_type: "timeout"&lt;/code&gt;, it knows the issue is transient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Zero. String classification.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 7: Circuit Breaker (pure code)
&lt;/h3&gt;

&lt;p&gt;Count failures per LLM provider. After 3 consecutive failures, skip that provider and try the fallback. Prevents the agent from burning through 120 seconds of timeout on a dead provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Zero. Counter + threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 8: HTTP Client Reuse (pure code)
&lt;/h3&gt;

&lt;p&gt;Store &lt;code&gt;*http.Client&lt;/code&gt; on the provider struct, reuse across calls. Previously each call created a new client, a new TCP connection, a new TLS handshake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Zero. Struct field.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;After Fix&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;th&gt;What Fixed It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;QL-001&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;td&gt;Needs pipeline architecture change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QL-002&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;td&gt;Already working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QL-003&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+40%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PrioritizeContext + PlanExecution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QL-004&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;td&gt;Already working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QL-005&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+60%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Max rounds + structured errors + conclusion preservation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Overall: 40% → 60%. Same model. Better input.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Latency went from 26s to 43s due to the PlanExecution LLM call (~3-5s per test). The HTTP reuse and circuit breaker savings show up under concurrent load, not in a 5-test sequential run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Didn't Improve — And Why
&lt;/h2&gt;

&lt;p&gt;QL-001 (multi-step creation) stayed at 0%. This isn't a context problem — it's a pipeline architecture problem. Each entity is created in isolation, and the IDs returned by each step are discarded before the next step runs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBVWyJVc2VyOiBDcmVhdGUgRW1iZXIgKyBSb2xhbmQ8YnIvPisgcml2YWxyeSBiZXR3ZWVuIHRoZW0iXQogICAgVSAtLT4gUzFbIlN0ZXAgMTxici8-Y3JlYXRlQ2hhcmFjdGVyKEVtYmVyKTxici8-4oaSIHJldHVybnMgZHJhZ29uX2lkXzQyIl0KICAgIFMxIC0uIHN0YXRlIGRpc2NhcmRlZCAuLT4gUzJbIlN0ZXAgMjxici8-Y3JlYXRlQ2hhcmFjdGVyKFJvbGFuZCk8YnIvPuKGkiByZXR1cm5zIGtuaWdodF9pZF83NyJdCiAgICBTMiAtLiBzdGF0ZSBkaXNjYXJkZWQgLi0-IFMzWyJTdGVwIDM8YnIvPmNyZWF0ZVJlbGF0aW9uc2hpcCg_LCA_KTxici8-bm8gSURzIGF2YWlsYWJsZSJdCiAgICBTMyAtLXggRlsiUmVsYXRpb25zaGlwIGZhaWxzPGJyLz5RTC0wMDE6IDAlIHBhc3MgcmF0ZSJd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBVWyJVc2VyOiBDcmVhdGUgRW1iZXIgKyBSb2xhbmQ8YnIvPisgcml2YWxyeSBiZXR3ZWVuIHRoZW0iXQogICAgVSAtLT4gUzFbIlN0ZXAgMTxici8-Y3JlYXRlQ2hhcmFjdGVyKEVtYmVyKTxici8-4oaSIHJldHVybnMgZHJhZ29uX2lkXzQyIl0KICAgIFMxIC0uIHN0YXRlIGRpc2NhcmRlZCAuLT4gUzJbIlN0ZXAgMjxici8-Y3JlYXRlQ2hhcmFjdGVyKFJvbGFuZCk8YnIvPuKGkiByZXR1cm5zIGtuaWdodF9pZF83NyJdCiAgICBTMiAtLiBzdGF0ZSBkaXNjYXJkZWQgLi0-IFMzWyJTdGVwIDM8YnIvPmNyZWF0ZVJlbGF0aW9uc2hpcCg_LCA_KTxici8-bm8gSURzIGF2YWlsYWJsZSJdCiAgICBTMyAtLXggRlsiUmVsYXRpb25zaGlwIGZhaWxzPGJyLz5RTC0wMDE6IDAlIHBhc3MgcmF0ZSJd" alt="mermaid diagram" width="1607" height="118"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fixing this requires collapsing the 5-layer pipeline into a unified agent with cross-step state — a larger architectural change, not a context fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Context optimization has a ceiling. Past that ceiling, you need architecture changes. But the ceiling is higher than most people think — we still had 20% improvement available before hitting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Still Missing
&lt;/h2&gt;

&lt;p&gt;Three pieces of infrastructure were built but not wired:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VerifyOutput&lt;/td&gt;
&lt;td&gt;Logs quality issues&lt;/td&gt;
&lt;td&gt;Doesn't retry on failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScoreMemoryUsage&lt;/td&gt;
&lt;td&gt;Computes relevance scores&lt;/td&gt;
&lt;td&gt;Scores never applied to future retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PlanExecution&lt;/td&gt;
&lt;td&gt;Generates plan before loop&lt;/td&gt;
&lt;td&gt;Plan not tracked during execution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three are &lt;strong&gt;open loops&lt;/strong&gt;. The infrastructure detects problems but doesn't act on them. Closing these loops is the next 20% — getting from 60% to 80%+.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Better input → better output. The LLM is the same.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your agent is underperforming, check the context before blaming the model. In our case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;7 out of 8 fixes were pure code&lt;/li&gt;
&lt;li&gt;Zero additional LLM cost (except one planning call at $0.003)&lt;/li&gt;
&lt;li&gt;20% quality improvement without changing the model&lt;/li&gt;
&lt;li&gt;The model was always capable — the context was holding it back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The highest-ROI investment in any agent system is context management. It's not glamorous. It's sort, filter, compress, truncate, prioritize. But it's the difference between 40% and 60% — and the foundation for everything else.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of the AI Agent Architecture series. See also: &lt;a href="https://harrisonsec.com/blog/ai-agent-90-percent-problem/" rel="noopener noreferrer"&gt;The 90% Problem&lt;/a&gt; for the broader framework, and &lt;a href="https://harrisonsec.com/blog/claude-code-context-engineering-compression-pipeline/" rel="noopener noreferrer"&gt;Claude Code Deep Dive Part 3&lt;/a&gt; for how Anthropic solves context at scale.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>contextmanagement</category>
      <category>engineering</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
