<?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: Gabriel Anhaia</title>
    <description>The latest articles on DEV Community by Gabriel Anhaia (@gabrielanhaia).</description>
    <link>https://dev.to/gabrielanhaia</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%2F425693%2F531a245a-bc24-453e-bb2b-eb7077a3da8b.png</url>
      <title>DEV Community: Gabriel Anhaia</title>
      <link>https://dev.to/gabrielanhaia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gabrielanhaia"/>
    <language>en</language>
    <item>
      <title>Your Go Fake Repository Drifts From Postgres. Run One Contract Suite Against Both</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Mon, 02 Nov 2026 06:01:15 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/your-go-fake-repository-drifts-from-postgres-run-one-contract-suite-against-both-3lm4</link>
      <guid>https://dev.to/gabrielanhaia/your-go-fake-repository-drifts-from-postgres-run-one-contract-suite-against-both-3lm4</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also by me:&lt;/strong&gt; &lt;em&gt;Pocket Guides for Developers&lt;/em&gt; — &lt;a href="https://www.amazon.com/dp/B0GYLH38X4" rel="noopener noreferrer"&gt;the 8-book series&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Your checkout tests are green. One of them replays a request the way a flaky mobile client would: same order reference, sent twice. The in-memory repository stores both orders without a word, so the use case's "reference already used" branch never runs and the test passes. In production, Postgres rejects the second insert with SQLSTATE &lt;a href="https://www.postgresql.org/docs/current/errcodes-appendix.html" rel="noopener noreferrer"&gt;&lt;code&gt;23505&lt;/code&gt;, &lt;code&gt;unique_violation&lt;/code&gt;&lt;/a&gt;, nothing maps it to a domain error, and the client gets a 500.&lt;/p&gt;

&lt;p&gt;Same suite, another test. The order-history endpoint should return the newest order first. The fake appends to a slice, the test happens to insert in the order it asserts, and it passes. The Postgres adapter forgot its &lt;code&gt;ORDER BY&lt;/code&gt;, and the &lt;a href="https://www.postgresql.org/docs/current/queries-order.html" rel="noopener noreferrer"&gt;Postgres docs&lt;/a&gt; are blunt about what that means: without sorting, "the rows will be returned in an unspecified order."&lt;/p&gt;

&lt;p&gt;Neither bug is in the fake's code. The fake never agreed to the same rules as the database. Martin Fowler's &lt;a href="https://martinfowler.com/bliki/TestDouble.html" rel="noopener noreferrer"&gt;definition of a fake&lt;/a&gt; already warns you: fakes "have working implementations, but usually take some shortcut." The fix he describes for doubles is a &lt;a href="https://martinfowler.com/bliki/ContractTest.html" rel="noopener noreferrer"&gt;contract test&lt;/a&gt;, which checks that calls against your double "return the same results as a call to the external service would." In a hexagonal Go service you can do that with one exported function and a single call per adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The port, with its rules written down
&lt;/h2&gt;

&lt;p&gt;The contract starts as prose on the interface. If a rule isn't written here, no adapter can be held to it.&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;package&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ErrNotFound&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order not found"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ErrDuplicateRef&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order ref already used"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ErrVersionConflict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order changed since read"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Order&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;ID&lt;/span&gt;         &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Ref&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;CustomerID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;CreatedAt&lt;/span&gt;  &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
    &lt;span class="n"&gt;Version&lt;/span&gt;    &lt;span class="kt"&gt;int64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// OrderRepository is the port. Every adapter must:&lt;/span&gt;
&lt;span class="c"&gt;//   - Create: store o with Version 1. ErrDuplicateRef if&lt;/span&gt;
&lt;span class="c"&gt;//     another order already has o.Ref.&lt;/span&gt;
&lt;span class="c"&gt;//   - Get: ErrNotFound if no order has that ID.&lt;/span&gt;
&lt;span class="c"&gt;//   - ListByCustomer: newest CreatedAt first, ties broken&lt;/span&gt;
&lt;span class="c"&gt;//     by ID in byte order.&lt;/span&gt;
&lt;span class="c"&gt;//   - Update: write o.Status if the stored Version equals&lt;/span&gt;
&lt;span class="c"&gt;//     o.Version, then bump it. ErrVersionConflict if not,&lt;/span&gt;
&lt;span class="c"&gt;//     ErrNotFound if the order does not exist.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ListByCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt; &lt;span class="kt"&gt;string&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="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The errors live in the domain because they are business facts. "That reference is taken" means something to a checkout use case. SQLSTATE &lt;code&gt;23505&lt;/code&gt; does not, and it should never cross the port.&lt;/p&gt;

&lt;h2&gt;
  
  
  One exported function, every adapter
&lt;/h2&gt;

&lt;p&gt;The suite goes in its own package, &lt;code&gt;domain/ordertest&lt;/code&gt;, in a normal &lt;code&gt;.go&lt;/code&gt; file rather than a &lt;code&gt;_test.go&lt;/code&gt; file. &lt;a href="https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies" rel="noopener noreferrer"&gt;&lt;code&gt;go build&lt;/code&gt; ignores files that end in &lt;code&gt;_test.go&lt;/code&gt;&lt;/a&gt;, so nothing declared in one can be imported by another package, and both adapter packages need to call this. Importing &lt;code&gt;testing&lt;/code&gt; from a non-test package has a precedent in the Go project itself: &lt;a href="https://pkg.go.dev/golang.org/x/net/nettest#TestConn" rel="noopener noreferrer"&gt;&lt;code&gt;nettest.TestConn&lt;/code&gt;&lt;/a&gt; "tests that a net.Conn implementation properly satisfies the interface," and it takes a constructor the same way.&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%2Fftogbyke7xk8powjdd94.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fftogbyke7xk8powjdd94.png" alt="One brass key ring hanging between two doors, a cardboard door and a steel door, both with the same lock" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One set of checks, two implementations. Each door has to open the same way.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The constructor gives each subtest a fresh, empty repository, so subtests never see each other's rows.&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;package&lt;/span&gt; &lt;span class="n"&gt;ordertest&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"slices"&lt;/span&gt;
    &lt;span class="s"&gt;"testing"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;

    &lt;span class="s"&gt;"example.com/shop/domain"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;RunOrderRepoContract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;newRepo&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;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderRepository&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="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"missing id"&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;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;newRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"nope"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"got %v, want ErrNotFound"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"round trip"&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;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;newRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;mustCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"R-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"o-1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"R-1"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
            &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"got %+v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&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="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"duplicate ref"&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;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;newRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&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;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;mustCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"R-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"R-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrDuplicateRef&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"got %v, want ErrDuplicateRef"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o-2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o-2 was stored anyway: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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;&lt;code&gt;t.Context()&lt;/code&gt; needs &lt;a href="https://pkg.go.dev/testing#T.Context" rel="noopener noreferrer"&gt;Go 1.24 or later&lt;/a&gt;. Timestamps are compared with &lt;code&gt;Equal&lt;/code&gt;, because the &lt;a href="https://pkg.go.dev/time#Time" rel="noopener noreferrer"&gt;&lt;code&gt;time&lt;/code&gt; docs&lt;/a&gt; note that &lt;code&gt;==&lt;/code&gt; "compares not just the time instant but also the Location," and a database adapter is free to hand back the same instant in a different location.&lt;/p&gt;

&lt;p&gt;The function continues with the ordering rule:&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;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"list order"&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;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;newRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;later&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hour&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;mustCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"R-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;mustCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"R-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;later&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;mustCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o-B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"R-3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;mustCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o-x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"R-4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;later&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListByCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"c-1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&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;got&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"o-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o-B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o-a"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"got %v, want %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&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 &lt;code&gt;list order&lt;/code&gt; subtest is built to fail a lazy fake. Insertion order is &lt;code&gt;o-a&lt;/code&gt;, &lt;code&gt;o-c&lt;/code&gt;, &lt;code&gt;o-B&lt;/code&gt;, and neither that order nor its reverse matches the expected &lt;code&gt;o-c&lt;/code&gt;, &lt;code&gt;o-B&lt;/code&gt;, &lt;code&gt;o-a&lt;/code&gt;. A fake that returns map iteration order fails too, at least some of the time, since the &lt;a href="https://go.dev/ref/spec#For_range" rel="noopener noreferrer"&gt;Go spec&lt;/a&gt; says map iteration order "is not specified and is not guaranteed to be the same from one iteration to the next." The uppercase &lt;code&gt;B&lt;/code&gt; is there on purpose, and it comes back in the Postgres section.&lt;/p&gt;

&lt;p&gt;The last two subtests cover updates, followed by the helpers:&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;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stale version"&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;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;newRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&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;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;mustCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"R-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o-1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"paid"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"cancelled"&lt;/span&gt;
        &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrVersionConflict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"got %v, want ErrVersionConflict"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o-1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"paid"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"got %+v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&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="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"update missing"&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;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;newRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"nope"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"R-9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"got %v, want ErrNotFound"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Ref&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomerID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cust&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="s"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;at&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;mustCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&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="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Helper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"create %s: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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 &lt;code&gt;stale version&lt;/code&gt; subtest is the optimistic-locking rule from &lt;a href="https://dev.to/gabrielanhaia/two-requests-edited-the-same-order-optimistic-locking-in-a-go-repository-adapter-2dnd"&gt;an earlier post on version conflicts in a Go repository adapter&lt;/a&gt;, folded into the shared suite instead of standing alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fake, held to it
&lt;/h2&gt;

&lt;p&gt;This map-backed fake passes the suite, and it is still small.&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;package&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"slices"&lt;/span&gt;
    &lt;span class="s"&gt;"strings"&lt;/span&gt;
    &lt;span class="s"&gt;"sync"&lt;/span&gt;

    &lt;span class="s"&gt;"example.com/shop/domain"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderRepo&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;mu&lt;/span&gt;    &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;
    &lt;span class="n"&gt;byID&lt;/span&gt;  &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;
    &lt;span class="n"&gt;byRef&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewOrderRepo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;byID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
        &lt;span class="n"&gt;byRef&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&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="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;byRef&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrDuplicateRef&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;byID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"memory: id %s exists"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;byID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;byRef&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&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;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&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="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;byID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotFound&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;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ListByCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt; &lt;span class="kt"&gt;string&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="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&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;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;byID&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&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="n"&gt;slices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SortFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&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;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&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;c&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&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="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;byID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotFound&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrVersionConflict&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;byID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;byRef&lt;/code&gt; index is the fake's version of a unique constraint, and the sort is its version of &lt;code&gt;ORDER BY&lt;/code&gt;. Without those two pieces, the duplicate and ordering bugs from the opening are exactly what you would have shipped.&lt;/p&gt;

&lt;p&gt;Wiring it to the suite is one call:&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;package&lt;/span&gt; &lt;span class="n"&gt;memory_test&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"testing"&lt;/span&gt;

    &lt;span class="s"&gt;"example.com/shop/adapter/memory"&lt;/span&gt;
    &lt;span class="s"&gt;"example.com/shop/domain"&lt;/span&gt;
    &lt;span class="s"&gt;"example.com/shop/domain/ordertest"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestOrderRepoContract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ordertest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunOrderRepoContract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderRepository&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;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewOrderRepo&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;h2&gt;
  
  
  The pgx adapter, mapping SQLSTATE 23505
&lt;/h2&gt;

&lt;p&gt;The real adapter uses &lt;a href="https://pkg.go.dev/github.com/jackc/pgx/v5" rel="noopener noreferrer"&gt;pgx v5&lt;/a&gt;. A failed insert carries a &lt;a href="https://pkg.go.dev/github.com/jackc/pgx/v5/pgconn#PgError" rel="noopener noreferrer"&gt;&lt;code&gt;*pgconn.PgError&lt;/code&gt;&lt;/a&gt; with a &lt;code&gt;Code&lt;/code&gt; (the SQLSTATE) and a &lt;code&gt;ConstraintName&lt;/code&gt;. Checking both matters: a primary-key collision is also &lt;code&gt;23505&lt;/code&gt;, and it is not the same domain fact as a reused reference.&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;package&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/jackc/pgx/v5"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/jackc/pgx/v5/pgconn"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/jackc/pgx/v5/pgxpool"&lt;/span&gt;

    &lt;span class="s"&gt;"example.com/shop/domain"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;`id, ref, customer_id, status, created_at, version`&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderRepo&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;pool&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pgxpool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewOrderRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pgxpool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;}&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;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
        INSERT INTO orders (`&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;`)
        VALUES ($1, $2, $3, $4, $5, 1)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pgErr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pgconn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PgError&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;As&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pgErr&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="n"&gt;pgErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"23505"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;pgErr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConstraintName&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"orders_ref_key"&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrDuplicateRef&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"create order %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="no"&gt;nil&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;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&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="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;`SELECT `&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;` FROM orders WHERE id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"get %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pgx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CollectExactlyOneRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scanOrder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pgx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNoRows&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="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotFound&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"get %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&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;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ListByCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt; &lt;span class="kt"&gt;string&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="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
        SELECT `&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;` FROM orders
         WHERE customer_id = $1
         ORDER BY created_at DESC, id COLLATE "C"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;customerID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"list %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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="n"&gt;pgx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CollectRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scanOrder&lt;/span&gt;&lt;span class="p"&gt;)&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;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`
        UPDATE orders SET status = $3, version = version + 1
         WHERE id = $1 AND version = $2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"update %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RowsAffected&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt; &lt;span class="c"&gt;// ErrNotFound, or a real failure&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrVersionConflict&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;scanOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="n"&gt;pgx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CollectableRow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&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;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://pkg.go.dev/github.com/jackc/pgx/v5#CollectExactlyOneRow" rel="noopener noreferrer"&gt;&lt;code&gt;CollectExactlyOneRow&lt;/code&gt;&lt;/a&gt; returns an error matching &lt;code&gt;pgx.ErrNoRows&lt;/code&gt; when the query finds nothing, which is the one place the adapter translates to &lt;code&gt;ErrNotFound&lt;/code&gt;. &lt;a href="https://pkg.go.dev/github.com/jackc/pgx/v5#CollectRows" rel="noopener noreferrer"&gt;&lt;code&gt;CollectRows&lt;/code&gt;&lt;/a&gt; closes the rows for you.&lt;/p&gt;

&lt;p&gt;Now the uppercase &lt;code&gt;B&lt;/code&gt;. The fake breaks ties with &lt;code&gt;strings.Compare&lt;/code&gt;, and the &lt;a href="https://go.dev/ref/spec#Comparison_operators" rel="noopener noreferrer"&gt;Go spec&lt;/a&gt; says strings "are compared lexically byte-wise." Postgres sorts text by the column's collation, and a linguistic collation such as &lt;code&gt;en_US.utf8&lt;/code&gt; does not have to put &lt;code&gt;o-B&lt;/code&gt; before &lt;code&gt;o-a&lt;/code&gt; the way byte order does. The &lt;a href="https://www.postgresql.org/docs/current/collation.html" rel="noopener noreferrer"&gt;&lt;code&gt;C&lt;/code&gt; collation&lt;/a&gt; sorts "by byte values rather than natural language order," so &lt;code&gt;COLLATE "C"&lt;/code&gt; on the tiebreak makes both adapters agree no matter what the database default is. Drop it, and the &lt;code&gt;list order&lt;/code&gt; subtest is where you find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Postgres, behind a build tag
&lt;/h2&gt;

&lt;p&gt;The integration test starts Postgres with the &lt;a href="https://golang.testcontainers.org/modules/postgres/" rel="noopener noreferrer"&gt;testcontainers-go Postgres module&lt;/a&gt;, runs the schema, and hands the suite a constructor that truncates the table. A &lt;a href="https://pkg.go.dev/cmd/go#hdr-Build_constraints" rel="noopener noreferrer"&gt;&lt;code&gt;//go:build&lt;/code&gt; line&lt;/a&gt; keeps it out of the default run.&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%2Fusva02ty393b5nzfcfqr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fusva02ty393b5nzfcfqr.png" alt="A workbench with a small wooden model engine and a real steel engine, both wired to the same burnt-orange test gauge" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The model and the real engine go on the same gauge. Only one of them needs Docker.&lt;/em&gt;&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="c"&gt;//go:build integration&lt;/span&gt;

&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;postgres_test&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"testing"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/jackc/pgx/v5/pgxpool"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/testcontainers/testcontainers-go"&lt;/span&gt;
    &lt;span class="n"&gt;tcpg&lt;/span&gt; &lt;span class="s"&gt;"github.com/testcontainers/testcontainers-go/modules/postgres"&lt;/span&gt;

    &lt;span class="s"&gt;"example.com/shop/adapter/postgres"&lt;/span&gt;
    &lt;span class="s"&gt;"example.com/shop/domain"&lt;/span&gt;
    &lt;span class="s"&gt;"example.com/shop/domain/ordertest"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;`
CREATE TABLE orders (
  id          text        PRIMARY KEY,
  ref         text        NOT NULL,
  customer_id text        NOT NULL,
  status      text        NOT NULL,
  created_at  timestamptz NOT NULL,
  version     bigint      NOT NULL,
  CONSTRAINT orders_ref_key UNIQUE (ref)
)`&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestOrderRepoContract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ctr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tcpg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"postgres:16-alpine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tcpg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shop"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;tcpg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithUsername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shop"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;tcpg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shop"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;tcpg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BasicWaitStrategies&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;testcontainers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CleanupContainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"sslmode=disable"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pgxpool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;ordertest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunOrderRepoContract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&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;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"TRUNCATE orders"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&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="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewOrderRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&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;&lt;code&gt;CleanupContainer&lt;/code&gt; goes before the error check because its &lt;a href="https://pkg.go.dev/github.com/testcontainers/testcontainers-go#CleanupContainer" rel="noopener noreferrer"&gt;docs&lt;/a&gt; say a nil container is a no-op, so a half-started container still gets removed. The module page also says it supplies no default wait strategy, which is why &lt;code&gt;BasicWaitStrategies()&lt;/code&gt; is there. One container serves all six subtests, and &lt;code&gt;TRUNCATE&lt;/code&gt; gives each one an empty table, which keeps the run to a single container start.&lt;/p&gt;

&lt;p&gt;The constraint is named explicitly, &lt;code&gt;orders_ref_key&lt;/code&gt;, because the adapter matches on that name. In a real service, replace the inline &lt;code&gt;schema&lt;/code&gt; with your migration runner, so the test checks the constraint names your migrations actually create.&lt;/p&gt;

&lt;p&gt;Two commands now cover both worlds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-race&lt;/span&gt; ./...                    &lt;span class="c"&gt;# fake only, no Docker&lt;/span&gt;
go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-race&lt;/span&gt; &lt;span class="nt"&gt;-tags&lt;/span&gt; integration ./...  &lt;span class="c"&gt;# fake and Postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the suite catches next
&lt;/h2&gt;

&lt;p&gt;The suite only knows the rules you gave it, and Postgres has more. Two worth adding as subtests before they bite:&lt;/p&gt;

&lt;p&gt;Timestamp precision. Go's &lt;code&gt;time.Time&lt;/code&gt; holds nanoseconds, and Postgres &lt;a href="https://www.postgresql.org/docs/current/datatype-datetime.html" rel="noopener noreferrer"&gt;&lt;code&gt;timestamptz&lt;/code&gt; resolution is 1 microsecond&lt;/a&gt;. The suite uses whole-second timestamps, so it can't see this. A subtest that creates an order with &lt;code&gt;time.Now()&lt;/code&gt; and compares &lt;code&gt;CreatedAt&lt;/code&gt; after a round trip will pass on the fake and, whenever the clock reading has sub-microsecond digits, fail on Postgres. Decide in the port whether the repository truncates, then make the fake do the same.&lt;/p&gt;

&lt;p&gt;Empty results. The two adapters in this post already disagree here. The fake above returns a &lt;code&gt;nil&lt;/code&gt; slice for a customer with no orders. The pgx adapter does not: &lt;a href="https://github.com/jackc/pgx/blob/master/rows.go" rel="noopener noreferrer"&gt;&lt;code&gt;CollectRows&lt;/code&gt;&lt;/a&gt; is &lt;code&gt;return AppendRows([]T{}, rows, fn)&lt;/code&gt;, so it starts from an empty, non-nil slice. Marshal both and the &lt;a href="https://pkg.go.dev/encoding/json#Marshal" rel="noopener noreferrer"&gt;&lt;code&gt;encoding/json&lt;/code&gt; docs&lt;/a&gt; say "a nil slice encodes as the null JSON value," so one adapter gives the client &lt;code&gt;null&lt;/code&gt; and the other &lt;code&gt;[]&lt;/code&gt;, and the suite as written passes both. Add a subtest that pins the shape, or normalise it in the handler.&lt;/p&gt;

&lt;p&gt;When a third adapter shows up (a read-through cache wrapping the Postgres repo, a SQLite build for a CLI), its test is one &lt;code&gt;RunOrderRepoContract&lt;/code&gt; call with a different constructor. If it disagrees with the other two, the suite names the rule.&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;A contract suite only works if the port says what the rules are and database errors stay in the adapter. &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt; is the book I wrote about that layer: where repositories, adapters and their tests sit in a Go service, and how to test both sides of a port without a pile of mocks. It is part of the &lt;em&gt;Thinking in Go&lt;/em&gt; series, with a companion book on the language itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.de/-/en/dp/B0GCYC79BQ" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2Fz4ypacn11k7zrkz179lt.jpg" alt="Thinking in Go — the 2-book series on Go programming and hexagonal architecture" width="401" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>postgres</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A 200K-Token Claude Session Left Idle Re-Buys Its Cache: $0.04 Becomes $1.00</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Mon, 02 Nov 2026 05:01:13 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/a-200k-token-claude-session-left-idle-re-buys-its-cache-004-becomes-100-3mfp</link>
      <guid>https://dev.to/gabrielanhaia/a-200k-token-claude-session-left-idle-re-buys-its-cache-004-becomes-100-3mfp</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&gt;Observability for LLM Applications&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also by me:&lt;/strong&gt; &lt;em&gt;Thinking in Go&lt;/em&gt; (2-book series) — &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; + &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Ten minutes at the coffee machine can add $0.96 to the next message in a 200,000-token &lt;a href="https://code.claude.com/docs/en/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; session. The session has been open since the morning and has read half the repo. You step away, come back, and type one line: "thanks, now also rename the test file."&lt;/p&gt;

&lt;p&gt;If you sign in with an API key and run Opus 5.5, the turn before your break read those 200K tokens from the prompt cache for about $0.04. Your one-line follow-up pays about $1.00 to write the same 200K tokens back into the cache, because the cache expired while you were gone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://code.claude.com/docs/en/changelog" rel="noopener noreferrer"&gt;Claude Code 2.1.314&lt;/a&gt;, out October 30, brought idle compaction to &lt;a href="https://code.claude.com/docs/en/vs-code" rel="noopener noreferrer"&gt;VS Code&lt;/a&gt; and the &lt;a href="https://code.claude.com/docs/en/desktop" rel="noopener noreferrer"&gt;Desktop&lt;/a&gt; Code tab to dodge exactly this bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three prices for the same token
&lt;/h2&gt;

&lt;p&gt;Every Claude Code turn is a fresh API request that re-sends the whole conversation; as the &lt;a href="https://code.claude.com/docs/en/prompt-caching.md" rel="noopener noreferrer"&gt;prompt caching docs&lt;/a&gt; put it, "The model doesn't remember anything between requests." The API skips reprocessing the unchanged start of the request, the prefix, when it matches what it recently cached. So a token in your history is billed at one of three rates, depending on the cache's state when the request arrives. Anthropic's &lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;prompt caching page&lt;/a&gt; gives the multipliers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A cache write with the 5-minute TTL costs 1.25x the base input price.&lt;/li&gt;
&lt;li&gt;A cache write with the 1-hour TTL costs 2x.&lt;/li&gt;
&lt;li&gt;A cache read is 0.1x, with exceptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exceptions matter for current models. The &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; prices cache hits on Opus 5.5 and Sonnet 5.5 at 0.05x, and on Fable 5.1 at 0.025x. On those models the gap between a warm read and a cold write is wider still.&lt;/p&gt;

&lt;p&gt;Each hit resets the timer, and the lifetime is "measured from the start of the request that writes or reads the cache entry." Work steadily and the cache stays warm all day. Step away past the TTL and the next request finds nothing, so it writes the whole prefix again at the write rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The math for one line at 200K tokens
&lt;/h2&gt;

&lt;p&gt;These are list prices per million tokens from Anthropic's &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;, multiplied by a 200,000-token prefix. Input side only: your new message and Claude's reply cost the same either way.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Warm read&lt;/th&gt;
&lt;th&gt;Cold, 5m write&lt;/th&gt;
&lt;th&gt;Cold, 1h write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5 ($4 base)&lt;/td&gt;
&lt;td&gt;$0.04&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;td&gt;$1.60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5 ($2 base)&lt;/td&gt;
&lt;td&gt;$0.02&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;$0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5.1 ($10 base)&lt;/td&gt;
&lt;td&gt;$0.05&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$4.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On Opus 5.5 the cold turn costs 25 times the warm one on the 5-minute TTL, and 40 times on the 1-hour TTL. On Fable 5.1 it is 50 and 80 times. There is no long-context surcharge hiding on top: the pricing page says Claude 4.6 and later models, except Haiku 5.5, bill the full 1M window "at standard pricing."&lt;/p&gt;

&lt;p&gt;Which column applies to you depends on how you pay. Claude Code's docs say it requests the one-hour TTL for the main conversation &lt;a href="https://code.claude.com/docs/en/prompt-caching.md#which-ttl-each-request-gets" rel="noopener noreferrer"&gt;only on a Claude subscription within your plan's included usage&lt;/a&gt;. With an API key, usage credits or a cloud provider, the default is five minutes. That means the coffee break above is enough.&lt;/p&gt;

&lt;p&gt;On a subscription you don't see dollars. You see plan usage, and a cold 200K rewrite draws on it the same way. Since a fix in &lt;a href="https://code.claude.com/docs/en/changelog" rel="noopener noreferrer"&gt;2.1.311&lt;/a&gt;, &lt;code&gt;/usage&lt;/code&gt; lists large prompt-cache misses under "What's contributing to your limits usage?".&lt;/p&gt;

&lt;p&gt;If you run on an API key and take breaks, the one-hour TTL can be the cheaper choice. A session that grew to 200K tokens by appending turns wrote roughly 200K tokens to the cache over its life, one exchange at a time. Under that assumption, paying the 1-hour rate instead of the 5-minute rate on those writes costs about 200K × ($8 − $5) per million, or $0.60 on Opus 5.5. One break of between five minutes and an hour then costs a $0.04 read instead of a $1.00 rewrite, which covers that premium. A break longer than an hour is cold on either TTL, and on the hour TTL the rewrite costs $1.60. Claude Code exposes the choice as the &lt;a href="https://code.claude.com/docs/en/settings-reference.md" rel="noopener noreferrer"&gt;&lt;code&gt;promptCacheTtl&lt;/code&gt;&lt;/a&gt; setting in &lt;code&gt;settings.json&lt;/code&gt; (v2.1.242 or later):&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"promptCacheTtl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1h"&lt;/span&gt;&lt;span class="w"&gt;
&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;&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%2Fz81botoy8kb5h4p3ldi4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz81botoy8kb5h4p3ldi4.png" alt="An hourglass whose falling sand is made of small token blocks, next to a coffee cup and a terminal window; below the empty bulb, a stack of burnt-orange coins" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The cache runs on a timer. When the sand runs out, the next message buys the whole history again.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  See it in your own numbers
&lt;/h2&gt;

&lt;p&gt;You don't have to trust my table. The API reports all three rates on every response. From the &lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;caching docs&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cache_creation_input_tokens&lt;/code&gt;: tokens written to the cache by this request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache_read_input_tokens&lt;/code&gt;: tokens read from the cache.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;input_tokens&lt;/code&gt;: tokens after the last cache breakpoint, neither read nor written.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total input is the sum of the three. A &lt;code&gt;cache_creation&lt;/code&gt; object splits the writes into &lt;code&gt;ephemeral_5m_input_tokens&lt;/code&gt; and &lt;code&gt;ephemeral_1h_input_tokens&lt;/code&gt;. This script uses the &lt;a href="https://github.com/anthropics/anthropic-sdk-python" rel="noopener noreferrer"&gt;Python SDK&lt;/a&gt; to send a large system block marked for caching, the way the caching docs show, and prints the counts. Point it at any text file over a few thousand tokens:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-5-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system&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;type&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;text&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;type&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;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;messages&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;role&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;user&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;content&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;thanks, now also...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;written:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache_creation_input_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read:   &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache_read_input_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fresh:  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The block has to be long enough to cache at all: 512 tokens on Opus 5.5, per the same docs, and shorter prompts are silently not cached. Run it twice inside five minutes and the second run should report the block under &lt;code&gt;read&lt;/code&gt;. Wait six minutes and run it again: the block moves back to &lt;code&gt;written&lt;/code&gt;. Multiply each count by its rate and you have the bill for that request.&lt;/p&gt;

&lt;p&gt;Claude Code reports the same counts. To check which TTL your main conversation's writes used, the docs suggest running &lt;code&gt;claude -p "hello" --output-format json&lt;/code&gt; and reading &lt;code&gt;usage.cache_creation&lt;/code&gt; in the result. During a session, &lt;code&gt;/usage&lt;/code&gt; (v2.1.251 or later) adds a &lt;code&gt;Prompt cache (main)&lt;/code&gt; line. The &lt;a href="https://code.claude.com/docs/en/costs.md" rel="noopener noreferrer"&gt;costs docs&lt;/a&gt; show it like this (one line in the terminal, wrapped here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt cache (main):   14 requests · 91% of input tokens from
cache · 2 misses (last 6m 10s ago, 310.2k tokens re-cached) ·
1 expected rebuild (compaction or tool-result clearing) ·
warm (1h TTL, last activity 40s ago)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The misses part says how many tokens cold turns wrote back, and a cold cache shows how long the session has been idle. If you want to go back further than &lt;code&gt;/usage&lt;/code&gt;, each session's full transcript is kept at &lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/&amp;lt;session&amp;gt;.jsonl&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/claude-directory.md" rel="noopener noreferrer"&gt;docs&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What idle compaction in 2.1.314 trades away
&lt;/h2&gt;

&lt;p&gt;Claude Code's answer to the coffee break is idle compaction. &lt;a href="https://code.claude.com/docs/en/changelog" rel="noopener noreferrer"&gt;2.1.299&lt;/a&gt; added it to the terminal on October 14 (Classmethod has a &lt;a href="https://dev.classmethod.jp/en/articles/20261014-cc-updates-v2-1-299/" rel="noopener noreferrer"&gt;write-up&lt;/a&gt;). On October 30, &lt;a href="https://code.claude.com/docs/en/changelog" rel="noopener noreferrer"&gt;2.1.314&lt;/a&gt; added it "to VS Code and Claude Desktop's Code tab: a 200K+ token conversation left idle is summarized, and the summary is used on your next message if the prompt cache has expired."&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://code.claude.com/docs/en/settings-reference.md" rel="noopener noreferrer"&gt;&lt;code&gt;idleCompaction&lt;/code&gt; entry&lt;/a&gt; lists when it runs. All of these must hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're in an interactive terminal session, the VS Code chat panel, or the Desktop Code tab running locally or over SSH.&lt;/li&gt;
&lt;li&gt;Auto-compaction is on.&lt;/li&gt;
&lt;li&gt;The conversation is on the one-hour TTL.&lt;/li&gt;
&lt;li&gt;Claude Code can fetch feature flags from Anthropic.&lt;/li&gt;
&lt;li&gt;The context has reached the threshold, 200,000 tokens by default.&lt;/li&gt;
&lt;li&gt;About an hour has passed since the last request, and your computer stayed awake.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the size condition against your model. The same entry says that "at the default, a conversation on a 200K context window auto-compacts before it reaches 200,000 tokens." So out of the box, only sessions on a larger context window ever qualify. On a 200K window you have to lower the threshold first (see below).&lt;/p&gt;

&lt;p&gt;Compaction sends a separate summarization request that repeats your history, and the docs say that while the cache is warm, it "reads your prefix from the cache." So reading 200K tokens happens at the cheap rate, before the cache expires. When you come back, your next message writes the summary and the files Claude Code re-reads, not 200K tokens.&lt;/p&gt;

&lt;p&gt;The summary isn't free. The summarization request reads the 200K prefix, about $0.04 on Opus 5.5. The summary itself is generated output, billed at the output rate ($20 per million on Opus 5.5). The request also inherits your extended thinking setting, and the &lt;a href="https://code.claude.com/docs/en/costs.md" rel="noopener noreferrer"&gt;costs docs&lt;/a&gt; say you can't turn thinking off on Opus 5.5 and that thinking tokens bill as output. If a summary came out at 10,000 tokens, that would be about $0.04 to read, $0.20 of output plus whatever thinking it spends, and $0.08 to write it to a 1-hour cache, with the re-read files on top. Treat that $0.32 as a floor, against $1.60 for the cold rewrite. Your summary size will differ. On a subscription, all of it is plan usage rather than dollars.&lt;/p&gt;

&lt;p&gt;What compaction really costs is detail. Per the &lt;a href="https://code.claude.com/docs/en/context-window.md" rel="noopener noreferrer"&gt;context window docs&lt;/a&gt;, project-root CLAUDE.md, auto memory and your plan are re-injected from disk, and Claude Code re-reads up to five recently modified files (a file over 5,000 tokens comes back as a path only). Path-scoped rules and nested CLAUDE.md files lived in the message history, so compaction "summarizes them away with everything else"; they come back only when Claude next touches a file that triggers them. Exact error messages and the dead ends you already ruled out go the same way, unless the summary happened to keep them.&lt;/p&gt;

&lt;p&gt;You'll know it happened. VS Code shows "Compacted using a summary written while you were away", and the terminal shows a line starting "Compacted while idle, before the prompt cache expired." To turn it off, set &lt;code&gt;"idleCompaction": false&lt;/code&gt;. To change the threshold, set &lt;code&gt;CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS&lt;/code&gt; in the &lt;code&gt;env&lt;/code&gt; block of &lt;code&gt;settings.json&lt;/code&gt;, with 100,000 as the floor.&lt;/p&gt;

&lt;p&gt;If you'd rather pick what survives, compact yourself before you step away. &lt;code&gt;/compact focus on the failing auth test&lt;/code&gt; keeps what you name. &lt;code&gt;/clear&lt;/code&gt; costs nothing when the next task is unrelated. And &lt;code&gt;/rewind&lt;/code&gt; goes back to a prefix that is already cached instead of building a new one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spotting the big sessions before you walk away
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt;, so weigh this section with that in mind. Hermes runs Claude Code, Codex and other agents side by side, each in a real terminal. With several sessions open, the question before you walk away is which of them are big.&lt;/p&gt;

&lt;p&gt;Hermes 2.3.4, released &lt;a href="https://github.com/hermes-hq/hermes-ide/releases" rel="noopener noreferrer"&gt;October 15&lt;/a&gt;, added a context meter to each session that shows how full the conversation is and offers Compact when it gets close to full. The sidebar shows a context gauge for each session, and the status strip shows the session's token total and its cost. That figure is either what the agent itself reports or an estimate from its transcript, labelled as an estimate.&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%2Fr3kzejmjo857hefcj20q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3kzejmjo857hefcj20q.png" alt="A Claude Code session in Hermes after pressing the Context chip, showing Claude Code's /context breakdown, per-session context gauges in the sidebar, and a status strip with the token total and estimated cost" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A Claude Code session after pressing the Context chip: Claude Code's /context breakdown (44.4k of 1M tokens, 4%), the per-session context gauges in the sidebar, and the status strip with the token total and estimated cost.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That gives you a habit for the break: scan the gauges, and for any session near 200K that sits between tasks, compact it with a focus before you leave. The Commands list above each session includes Compact, and a command only runs when the agent is waiting, so it never lands in the middle of a turn. When you come back, watch how far the session's spend figure jumps after your first message; that jump is roughly what the return cost.&lt;/p&gt;

&lt;p&gt;What Hermes doesn't do: it doesn't know whether a session's prompt cache is warm or cold, or which TTL it is on. For that, run &lt;code&gt;/usage&lt;/code&gt; inside Claude Code and read the &lt;code&gt;Prompt cache (main)&lt;/code&gt; line. Hermes also doesn't compact anything on its own. You click Compact, or Claude Code's own idle compaction does 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%2Fyu9ijyk77c8da219fk68.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyu9ijyk77c8da219fk68.png" alt="The action bar above a Claude Code session in Hermes with the Commands list open, showing pinned Context, Model, Plan and Review, then built-in commands such as Compact, Usage and Diff" width="800" height="398"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The action bar above a Claude Code session (Prompts, Context 4%, Model, Plan, Review, / Commands) with the Commands list open: pinned Context, Model, Plan and Review, then built-in commands such as Compact, Usage and Diff.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hermes is free to &lt;a href="https://hermes-ide.com/download" rel="noopener noreferrer"&gt;download&lt;/a&gt;, source-available under BSL 1.1, and runs on macOS, Windows and Linux. The code is on &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. It isn't affiliated with Anthropic, Nous Research or any agent vendor.&lt;/p&gt;

&lt;p&gt;Do you let idle compaction summarize your long sessions, or did you turn it off the first time it dropped something you needed?&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;A cold cache is a cost regression you only see if you trace tokens per request and split reads from writes. &lt;em&gt;Observability for LLM Applications&lt;/em&gt; is my book on doing that for LLM features in production: traces, evals, cost and quality, and the dashboards that catch a $0.04 turn quietly becoming a $1.00 one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2F088uxlv56opt1f0pnz3i.jpg" alt="Observability for LLM Applications — the book" width="334" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>llm</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Build a BPE Tokenizer in 80 Lines of TypeScript and Watch JSON Eat Your Tokens</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:01:17 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/build-a-bpe-tokenizer-in-80-lines-of-typescript-and-watch-json-eat-your-tokens-leg</link>
      <guid>https://dev.to/gabrielanhaia/build-a-bpe-tokenizer-in-80-lines-of-typescript-and-watch-json-eat-your-tokens-leg</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0HBN9XMCY" rel="noopener noreferrer"&gt;AI That Answers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The series:&lt;/strong&gt; &lt;em&gt;AI in TypeScript&lt;/em&gt; — 5 books, from your first LLM call to agents in production — &lt;a href="https://xgabriel.com/ai-in-typescript" rel="noopener noreferrer"&gt;all five here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The maintainers of the TOON format ran three flat datasets through OpenAI's &lt;code&gt;o200k_base&lt;/code&gt; encoding and counted &lt;a href="https://github.com/toon-format/toon" rel="noopener noreferrer"&gt;164,643 tokens for the pretty-printed JSON and 104,908 for the same data minified&lt;/a&gt;. Nothing was dropped. The minified version holds every key and every value. It stops paying for indentation and line breaks, and that alone takes about 36% off the count.&lt;/p&gt;

&lt;p&gt;Tokens are the unit your LLM bill is written in. &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;Anthropic's pricing table&lt;/a&gt;, for one, quotes input and output separately, per million tokens. Yet most developers who pay that bill have never watched a string turn into tokens. You can build the machine that does it in 80 lines of TypeScript and train it on your own JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens to your prompt before the model sees it
&lt;/h2&gt;

&lt;p&gt;OpenAI's tokenizers use byte-pair encoding. BPE began as a compression algorithm, and &lt;a href="https://arxiv.org/abs/1508.07909" rel="noopener noreferrer"&gt;Sennrich, Haddow and Birch adapted it to split words into subword units&lt;/a&gt; for neural machine translation (ACL 2016). The &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;tiktoken README&lt;/a&gt; says the encoding is reversible and lossless and works on any text, even text the tokenizer never saw in training. On average, a token covers about 4 bytes.&lt;/p&gt;

&lt;p&gt;Encoding runs in three steps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A regex cuts the text into chunks, so letters, digits, punctuation and whitespace end up in separate runs. &lt;a href="https://github.com/karpathy/minbpe" rel="noopener noreferrer"&gt;Karpathy's minbpe&lt;/a&gt; credits this to the GPT-2 paper and notes it "ensures that no merges will happen across category boundaries."&lt;/li&gt;
&lt;li&gt;Each chunk becomes its UTF-8 bytes. The starting vocabulary is the 256 possible byte values, so nothing is ever out of vocabulary.&lt;/li&gt;
&lt;li&gt;Learned merges glue adjacent byte pairs into bigger tokens, in the order they were learned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Training is where the merges come from. Count every adjacent pair across the corpus, take &lt;a href="https://github.com/karpathy/minbpe/blob/master/minbpe/basic.py" rel="noopener noreferrer"&gt;the pair with the highest count&lt;/a&gt;, give it the next free id (256, then 257, and so on), replace it everywhere, repeat. The minbpe README walks through &lt;code&gt;aaabdaaabac&lt;/code&gt;: three merges (&lt;code&gt;aa&lt;/code&gt;, then &lt;code&gt;ab&lt;/code&gt;, then the pair of those two new tokens) turn 11 bytes into five tokens, &lt;code&gt;[258, 100, 258, 97, 99]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feya3r83mq6nvsip3u582.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feya3r83mq6nvsip3u582.png" alt="Rows of small square byte tiles fusing pairwise into larger tiles, layer by layer" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Byte-pair encoding: start from single bytes, then fuse the most frequent neighbours, one merge at a time.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The tokenizer in 80 lines
&lt;/h2&gt;

&lt;p&gt;This is the whole trainer and encoder. It uses GPT-2's pre-split pattern, minus the contraction rules (&lt;code&gt;'s&lt;/code&gt;, &lt;code&gt;'ll&lt;/code&gt;, and friends), copied from &lt;a href="https://github.com/karpathy/minbpe/blob/master/minbpe/regex.py" rel="noopener noreferrer"&gt;minbpe's regex.py&lt;/a&gt;.&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;// bpe.ts: a toy byte-level BPE trainer and encoder&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Pair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// GPT-2's pre-split pattern, minus its contraction rules&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SPLIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="sr"&gt;/ &lt;/span&gt;&lt;span class="se"&gt;?\p&lt;/span&gt;&lt;span class="sr"&gt;{L}+| &lt;/span&gt;&lt;span class="se"&gt;?\p&lt;/span&gt;&lt;span class="sr"&gt;{N}+| &lt;/span&gt;&lt;span class="se"&gt;?[^\s\p&lt;/span&gt;&lt;span class="sr"&gt;{L}&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="sr"&gt;{N}&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+|&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?!\S)&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/gu&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;utf8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&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;text8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;show&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&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;chunksOf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SPLIT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;countPairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&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;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counts&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="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;Pair&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ids&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="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;out&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the pair's second half is consumed too&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;out&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;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&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;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;numMerges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chunksOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;merges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;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;vocab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;let&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;vocab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&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;let&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;numMerges&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="o"&gt;++&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;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;ids&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;countPairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bestCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// a pair seen once earns no token&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bestCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;bestCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&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="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;best&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nx"&gt;Pair&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;ids&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;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;merges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&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;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vocab&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="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vocab&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="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nx"&gt;vocab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bestCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;x`&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;merges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vocab&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&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;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;merges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;chunksOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// earliest-learned merge first, the order training used&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Pair&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;Infinity&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;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;merges&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="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nx"&gt;pair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
        &lt;span class="p"&gt;}&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;pair&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="nx"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;out&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;ids&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="nx"&gt;out&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;Pairs are keyed as &lt;code&gt;"a,b"&lt;/code&gt; strings because a &lt;code&gt;Map&lt;/code&gt; compares array keys by reference, so &lt;code&gt;[1, 2]&lt;/code&gt; would never find &lt;code&gt;[1, 2]&lt;/code&gt;. Ties go to the pair seen first, since &lt;code&gt;Map&lt;/code&gt; iterates in insertion order and the comparison is strict. Training works chunk by chunk, so a merge can never span a letter and a digit, or a word and the punctuation after it, which matches the real thing. And &lt;code&gt;encode&lt;/code&gt; always applies the lowest-ranked merge available, the same rule minbpe's &lt;code&gt;_encode_chunk&lt;/code&gt; uses: replay the merges in the order training learned them.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;show&lt;/code&gt; helper decodes a token's bytes for printing. When a token holds only part of a multi-byte character, &lt;code&gt;TextDecoder&lt;/code&gt; prints &lt;code&gt;�&lt;/code&gt;, because the token boundary falls inside the character. The Japanese sample further down does the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Train it on JSON and read the merges
&lt;/h2&gt;

&lt;p&gt;Feed it the kind of text your app actually sends: an array of records, pretty-printed.&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;// demo.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./bpe&lt;/span&gt;&lt;span class="dl"&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;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&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="s2"&gt;suspended&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DE&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="s2"&gt;BR&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="s2"&gt;JP&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="s2"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&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;pretty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;merges&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pretty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&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;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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;asPretty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;merges&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;asMin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;one&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;merges&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pretty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;asPretty&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="na"&gt;minified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;asMin&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;a href="https://github.com/privatenumber/tsx" rel="noopener noreferrer"&gt;tsx&lt;/a&gt;: &lt;code&gt;npx tsx demo.ts&lt;/code&gt;. Each log line is one merge, with the two parts, the new token, and how often the pair appeared. Read all 40 and sort them in your head: how many are fragments of the keys &lt;code&gt;customer_id&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;country&lt;/code&gt;, how many are values and digits, and how many are quotes, colons and runs of spaces? On a corpus made of nothing but JSON, every merge has to come from one of those. The keys repeat in every single record, and that repetition is what earns them merges. A production tokenizer learns its merges from a huge general corpus instead, so common English words have a good chance of getting a token of their own (the next script shows you whether &lt;code&gt;customer&lt;/code&gt; did). Your &lt;code&gt;acctBalanceAdjFlagV2&lt;/code&gt; pays for every piece.&lt;/p&gt;

&lt;p&gt;Then try the experiments the script makes cheap. Train on &lt;code&gt;JSON.stringify(records)&lt;/code&gt; instead of the pretty version and compare the merge log. Rename &lt;code&gt;customer_id&lt;/code&gt; to &lt;code&gt;cid&lt;/code&gt; and see how many merges it frees up. Compare the two counts in the last line, and remember that this toy only knows 40 merges, so the ratio between them tells you more than either number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask a real encoder: o200k_base
&lt;/h2&gt;

&lt;p&gt;Your toy has 40 merges. &lt;code&gt;o200k_base&lt;/code&gt; has a vocabulary of about 200,000 tokens, roughly double GPT-4's ~100,000, according to &lt;a href="https://simonwillison.net/2024/May/13/gpt-4o" rel="noopener noreferrer"&gt;Simon Willison's notes from the GPT-4o launch&lt;/a&gt;. tiktoken's &lt;a href="https://github.com/openai/tiktoken/blob/main/tiktoken/model.py" rel="noopener noreferrer"&gt;model table&lt;/a&gt; maps &lt;code&gt;gpt-4o&lt;/code&gt;, &lt;code&gt;gpt-4.1&lt;/code&gt;, &lt;code&gt;o3&lt;/code&gt;, &lt;code&gt;o4-mini&lt;/code&gt; and &lt;code&gt;gpt-5&lt;/code&gt; to it. &lt;a href="https://github.com/dqbd/tiktoken/blob/main/js/README.md" rel="noopener noreferrer"&gt;js-tiktoken&lt;/a&gt; loads it in Node; the lite import pulls in only the one rank file you need.&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;// compare.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Tiktoken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;js-tiktoken/lite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;o200k_base&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;js-tiktoken/ranks/o200k_base&lt;/span&gt;&lt;span class="dl"&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;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Tiktoken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o200k_base&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;pieces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;id&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;samples&lt;/span&gt; &lt;span class="o"&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;{&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;  "customer_id": 1000&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;}&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;{"customer_id":1000}&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="s2"&gt;customerShippingAddressLine2&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="s2"&gt;1730505600000&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="s2"&gt;Your order has been cancelled.&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="s2"&gt;ご注文はキャンセルされました。&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="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;s&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;samples&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pieces&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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;You can predict part of what it prints by reading the o200k pre-split regex in &lt;a href="https://github.com/openai/tiktoken/blob/main/tiktoken_ext/openai_public.py" rel="noopener noreferrer"&gt;tiktoken's openai_public.py&lt;/a&gt;. Every chunk the regex produces costs at least one token, so the regex sets a floor.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;camelCase splits at every capital.&lt;/strong&gt; The letter rule matches a run of uppercase letters (possibly none) followed by lowercase, so &lt;code&gt;customerShippingAddressLine2&lt;/code&gt; cuts into &lt;code&gt;customer&lt;/code&gt;, &lt;code&gt;Shipping&lt;/code&gt;, &lt;code&gt;Address&lt;/code&gt;, &lt;code&gt;Line&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt; before BPE starts. Five chunks, at least five tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digits go in groups of three.&lt;/strong&gt; The number rule is &lt;code&gt;\p{N}{1,3}&lt;/code&gt;. A 13-digit millisecond timestamp is five chunks, so at least five tokens, every time it appears in a log line you paste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whitespace is its own chunk.&lt;/strong&gt; Indentation runs, minus their last space, are cut out separately from the keys they sit in front of. One small quirk: the space after &lt;code&gt;"customer_id":&lt;/code&gt; in the pretty version cannot attach to a following number, because the digit rule (&lt;code&gt;\p{N}{1,3}&lt;/code&gt;) takes no leading character and the punctuation rule's optional space needs a symbol after it. It becomes a chunk on its own. Before a string value it rides along with the opening quote instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-English text gets fewer merges.&lt;/strong&gt; Byte-level BPE can encode anything, but the merges go to whatever was common in training data. &lt;a href="https://arxiv.org/abs/2305.15425" rel="noopener noreferrer"&gt;Petrov et al. (NeurIPS 2023)&lt;/a&gt; found the same text translated into different languages could differ in token length by up to 15 times on the tokenizers they studied. OpenAI's larger vocabulary cut non-English counts: its launch figures, as quoted in Willison's notes, put Gujarati at 4.4x fewer tokens and Japanese at 1.4x fewer than the previous tokenizer. When a token boundary still lands inside a Japanese character, &lt;code&gt;pieces&lt;/code&gt; prints &lt;code&gt;�&lt;/code&gt; for those fragments.&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%2Fsfi5h5vvgkyf3dk553rn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsfi5h5vvgkyf3dk553rn.png" alt="A long accordion of curly braces and indentation blocks next to a compact stack of the same blocks" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Same data, same keys and values; only the whitespace and repetition change, and that is where the tokens go.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  One payload, three shapes
&lt;/h2&gt;

&lt;p&gt;Now measure the thing you actually send. This script builds one payload and counts it as pretty JSON, minified JSON and &lt;a href="https://github.com/toon-format/toon" rel="noopener noreferrer"&gt;TOON&lt;/a&gt;, a tabular encoding of the JSON data model that declares the keys of a uniform array once (&lt;a href="https://www.npmjs.com/package/@toon-format/toon" rel="noopener noreferrer"&gt;&lt;code&gt;@toon-format/toon&lt;/code&gt;&lt;/a&gt; on npm).&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;// shapes.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Tiktoken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;js-tiktoken/lite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;o200k_base&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;js-tiktoken/ranks/o200k_base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;encode&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nx"&gt;toToon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@toon-format/toon&lt;/span&gt;&lt;span class="dl"&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;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Tiktoken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o200k_base&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;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shipped&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="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;totalCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1999&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;37&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orders&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;shapes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pretty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;minified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;toon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;toToon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shapes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;payload&lt;/code&gt; for a real request body from your logs and you have your own numbers. I can't give you numbers for your data, so here are published ones for theirs. &lt;a href="https://github.com/toon-format/toon" rel="noopener noreferrer"&gt;TOON's benchmark&lt;/a&gt;, counted with &lt;code&gt;o200k_base&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Track&lt;/th&gt;
&lt;th&gt;Pretty JSON&lt;/th&gt;
&lt;th&gt;Minified JSON&lt;/th&gt;
&lt;th&gt;TOON&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flat data, 3 datasets&lt;/td&gt;
&lt;td&gt;164,643&lt;/td&gt;
&lt;td&gt;104,908&lt;/td&gt;
&lt;td&gt;68,030&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed structure, 5 datasets&lt;/td&gt;
&lt;td&gt;393,637&lt;/td&gt;
&lt;td&gt;260,451&lt;/td&gt;
&lt;td&gt;264,734&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On flat, uniform records TOON came in 58.7% under pretty JSON and 35.2% under minified. On mixed and nested data the picture flips: minified JSON beat TOON by 1.6% overall, and the README says plainly that "for deeply nested or non-uniform data, JSON may be more efficient." It also notes that absolute counts are tokenizer-specific, since other providers tokenize differently, and that you have to measure latency yourself. Fewer tokens is also not the same as better answers, so run your evals on the new shape before you ship it.&lt;/p&gt;

&lt;p&gt;One win held on both tracks: minifying the JSON cut about a third of the tokens, so do that to anything a program produced before it goes into a prompt. Dropping keys the model does not need helps whatever the shape. Save the tabular format for long arrays of identical objects, where TOON won, and keep JSON for nested data, where it lost. Then put &lt;code&gt;shapes.ts&lt;/code&gt; next to your prompt builder and check the count every time someone adds a field.&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;Token cost is the first bill an LLM app runs up, and it is one of the threads in &lt;a href="https://www.amazon.com/dp/B0HBN9XMCY" rel="noopener noreferrer"&gt;&lt;em&gt;AI That Answers&lt;/em&gt;&lt;/a&gt;, the first book of my &lt;em&gt;AI in TypeScript&lt;/em&gt; series. It covers your first LLM app in TypeScript end to end: prompts, structured output, and keeping an eye on what each call costs before the invoice does it for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/dp/B0HBN9XMCY" rel="noopener noreferrer"&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%2F3mh2ydn0v9owkcblvjvj.png" alt="AI That Answers — AI in TypeScript" width="600" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The other four books go from retrieval and tool calling to agents in production: &lt;a href="https://xgabriel.com/ai-in-typescript" rel="noopener noreferrer"&gt;the whole &lt;em&gt;AI in TypeScript&lt;/em&gt; series is here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>llm</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Agents Made Rewrites Cheap to Type. Spolsky's 2000 Warning Was Never About Typing</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Mon, 02 Nov 2026 03:01:18 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/agents-made-rewrites-cheap-to-type-spolskys-2000-warning-was-never-about-typing-4d0</link>
      <guid>https://dev.to/gabrielanhaia/agents-made-rewrites-cheap-to-type-spolskys-2000-warning-was-never-about-typing-4d0</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0H1WVFBYY" rel="noopener noreferrer"&gt;Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also by me:&lt;/strong&gt; &lt;em&gt;Thinking in Go&lt;/em&gt; (2-book series) — &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; + &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;In February, two rewrites made the rounds. Anthropic had 16 Claude agents write a 100,000-line C compiler in Rust from scratch: nearly 2,000 Claude Code sessions over two weeks and just under $20,000 in API cost (&lt;a href="https://www.anthropic.com/engineering/building-c-compiler" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;). Nineteen days later Cloudflare announced vinext, a reimplementation of the &lt;a href="https://nextjs.org" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; API on &lt;a href="https://vite.dev" rel="noopener noreferrer"&gt;Vite&lt;/a&gt;: "one engineer and an AI model rebuilt the most popular front-end framework from scratch." It took more than 800 sessions in &lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt; and roughly $1,100 in Claude API tokens (&lt;a href="https://blog.cloudflare.com/vinext/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Read those numbers and the old rule against rewrites looks dead. If a framework costs $1,100, your 4,000-line shipping-quote module costs a lunch.&lt;/p&gt;

&lt;p&gt;Look at what both projects had before an agent typed a line, though. The compiler team had the GCC torture test suite. The vinext team ported tests straight out of the Next.js repository. Each rewrite was graded by a test suite that other people had spent years writing. Your shipping-quote module doesn't have one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Spolsky actually said in 2000
&lt;/h2&gt;

&lt;p&gt;Joel Spolsky published &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;Things You Should Never Do, Part I&lt;/a&gt; on April 6, 2000. Netscape 6.0 was going into its first public beta. "There never was a version 5.0," he wrote, and version 4.0 had shipped almost three years earlier. Netscape, in his words, had made "the single worst strategic mistake that any software company can make": it rewrote the code from scratch.&lt;/p&gt;

&lt;p&gt;The essay is easy to remember as "rewrites take too long." He does make that argument: starting over means "giving a gift of two or three years to your competitors." Agents really do shrink that part. The core of the essay is about what the old code knows. That ugly function "has grown little hairs and stuff on it and nobody knows why." Each hair is a fix: a &lt;code&gt;LoadLibrary&lt;/code&gt; call that keeps the code running on old versions of Windows 95, a branch for "that bug that occurs in low memory conditions." "Each of these bugs took weeks of real-world usage before they were found."&lt;/p&gt;

&lt;p&gt;Then the sentence that matters for 2026: "When you throw away code and start from scratch, you are throwing away all that knowledge. All those collected bug fixes."&lt;/p&gt;

&lt;p&gt;That second argument is the one agents leave untouched. An agent shrinks the typing to an afternoon, and the knowledge it throws away is the same size as in 2000.&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%2F5zctcwnbmvjyfxx4m30c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5zctcwnbmvjyfxx4m30c.png" alt="A gnarled old tree cut open to show growth rings packed with small patched bugs, while a robot arm plants a clean new sapling beside the stump" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every ring is a fix someone found the hard way. The sapling starts with none of them.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The famous agent rewrites borrowed their memory
&lt;/h2&gt;

&lt;p&gt;The compiler write-up is honest about where the work went. The author says the task verifier has to be "nearly perfect, otherwise Claude will solve the wrong problem," and that most of the effort went into the tests, the environment and the feedback around the agents. Near the end, the agents kept breaking things that already worked, so a stricter CI gate went in to stop new commits from breaking existing code. When the Linux kernel build stalled, GCC itself became "an online known-good compiler oracle to compare against" (&lt;a href="https://www.anthropic.com/engineering/building-c-compiler" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Even with all that, the result is a research artifact. The same post says it has no assembler or linker of its own, calls GCC for 16-bit x86 code, generates inefficient code, and isn't a drop-in replacement for a production compiler.&lt;/p&gt;

&lt;p&gt;vinext followed the same pattern. Cloudflare reports over 1,700 Vitest tests and 380 Playwright end-to-end tests, including tests ported directly from the Next.js suite, with CI running all of it on every pull request. It still calls the project experimental, "not yet been battle-tested with any meaningful traffic at scale," with 94% of the Next.js 16 API surface covered (&lt;a href="https://blog.cloudflare.com/vinext/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Both teams turned someone else's accumulated knowledge into an executable oracle, and let the agents grind against it. That's the part to copy.&lt;/p&gt;
&lt;h2&gt;
  
  
  The cost moved into the tests that don't exist yet
&lt;/h2&gt;

&lt;p&gt;Your internal module has no GCC and no public test suite. Its knowledge lives in three places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Branches in the code whose reason nobody wrote down.&lt;/li&gt;
&lt;li&gt;Commit messages and tickets that explain some of those branches.&lt;/li&gt;
&lt;li&gt;Production, where customers hit the cases nobody remembers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Spolsky's axiom from the same essay still applies: "It's harder to read code than to write it." Agents read fast, and they will happily summarize what a branch does. They can't recover why it exists. A rewrite prompt like "clean this up, keep the behavior" gets you code that keeps the behavior the agent noticed. The edge case it skipped looks like dead code, and an agent asked for clean code deletes dead code.&lt;/p&gt;

&lt;p&gt;So the rewrite is still expensive; you pay in the tests you have to write before the typing starts, because those tests are the only form of the old knowledge an agent can check itself against.&lt;/p&gt;

&lt;p&gt;Michael Feathers named this kind of test in &lt;em&gt;Working Effectively with Legacy Code&lt;/em&gt;: a &lt;a href="https://en.wikipedia.org/wiki/Characterization_test" rel="noopener noreferrer"&gt;characterization test&lt;/a&gt; describes "the actual behavior of an existing piece of software," right or wrong. People also call it a golden master.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pin the old code before any agent touches it
&lt;/h2&gt;

&lt;p&gt;Mine the history first. Every commit that touched the module is a candidate edge case, and the hairy lines tell you which commits matter:&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;# every commit that touched the module, oldest first&lt;/span&gt;
git log &lt;span class="nt"&gt;--reverse&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%h %ad %s'&lt;/span&gt; &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; src/Legacy/ShippingQuote.php

&lt;span class="c"&gt;# which commit last changed each line, whitespace ignored&lt;/span&gt;
git blame &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; 40,90 src/Legacy/ShippingQuote.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turn each fix you find into a named input. Name it after the commit, so the next person knows where it came from:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"domestic_standard"&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;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight_g"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"zero_weight_voucher"&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;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight_g"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"island_zip"&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;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"18565"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                 &lt;/span&gt;&lt;span class="nl"&gt;"weight_g"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fix_4f2a91c_rounding"&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;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight_g"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2001&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put the old class and the rewrite behind the same small interface, a port, so the recording and the test call the same method. Then record what the old code returns, once, on &lt;code&gt;main&lt;/code&gt;, and commit the recording:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// tests/golden/record.php&lt;/span&gt;
&lt;span class="c1"&gt;// Run once on main, before any agent touches the module.&lt;/span&gt;
&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/../../vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Shipping\LegacyQuoter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/inputs.json'&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="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;JSON_THROW_ON_ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$legacy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LegacyQuoter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$inputs&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$legacy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/expected.json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;JSON_PRETTY_PRINT&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;JSON_PRESERVE_ZERO_FRACTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&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 test replays every input against the rewrite and demands the exact same answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// tests/ShippingGoldenMasterTest.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Shipping\RewrittenQuoter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;PHPUnit\Framework\Attributes\DataProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;PHPUnit\Framework\TestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShippingGoldenMasterTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;TestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;iterable&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/golden/inputs.json'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$want&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/golden/expected.json'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$in&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$want&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$name&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="na"&gt;#[DataProvider('cases')]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testRewriteMatchesLegacy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RewrittenQuoter&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;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Same JSON round trip the recording went through.&lt;/span&gt;
        &lt;span class="nv"&gt;$got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JSON_PRESERVE_ZERO_FRACTION&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$got&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&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="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;JSON_THROW_ON_ERROR&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;&lt;code&gt;assertSame&lt;/code&gt; is strict on purpose, and &lt;code&gt;JSON_PRESERVE_ZERO_FRACTION&lt;/code&gt; keeps floats as floats through the JSON files (&lt;a href="https://www.php.net/manual/en/json.constants.php" rel="noopener noreferrer"&gt;PHP manual&lt;/a&gt;). If the old code returned &lt;code&gt;5.0&lt;/code&gt; and the rewrite returns &lt;code&gt;5&lt;/code&gt;, that's a behavior change, and you want to decide about it, not discover it in an invoice. If a recorded answer is a bug you want fixed, fix it in a separate commit after the rewrite lands, so "same behavior" and "better behavior" never share a diff.&lt;/p&gt;

&lt;p&gt;I walked through a longer version of this on a Laravel controller, with frozen time and a mutation check, in &lt;a href="https://dev.to/gabrielanhaia/pin-a-legacy-laravel-controller-with-golden-master-tests-then-extract-the-use-case-3dkd"&gt;Pin a Legacy Laravel Controller With Golden-Master Tests&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvmgjbro61m1hfdghzvzy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvmgjbro61m1hfdghzvzy.png" alt="A balance scale weighing an old brass machine against a sleek new one, both dropping identical orange tokens into matching trays" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The golden master asks one question: same input, same output, for every case the old code learned.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Spike the rewrite where throwing it away is cheap
&lt;/h2&gt;

&lt;p&gt;With the recording committed, the rewrite is a spike. Let the agent try. If it can't get the golden master green, you delete a folder and lose nothing.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt;, so weigh this part with that in mind. In Hermes, ⌘N (Ctrl+Shift+N on Windows and Linux) starts each task on its own &lt;code&gt;hermes/&amp;lt;name&amp;gt;&lt;/code&gt; branch in its own worktree, with the task as the agent's first prompt (&lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;README&lt;/a&gt;). A worktree recipe in &lt;code&gt;.hermes/worktree.toml&lt;/code&gt; prepares the fresh checkout and sets the default checks for when a task is done (&lt;a href="https://github.com/hermes-hq/hermes-ide/blob/main/docs/worktree-recipes.md" rel="noopener noreferrer"&gt;recipe docs&lt;/a&gt;). Put the golden master there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# .hermes/worktree.toml&lt;/span&gt;
&lt;span class="n"&gt;setup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"composer install"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;done_when&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"vendor/bin/phpunit --filter ShippingGoldenMasterTest"&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;With that config, the default done check for every task is the recording from the old code, whatever the agent reports.&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%2Fyes6cfacd13syaf30wf7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyes6cfacd13syaf30wf7.png" alt="The Review Desk's Worktrees tab in Hermes IDE, listing one worktree each for orders-api, web-app and orders-types with branch, task, age and disk size" width="800" height="332"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Review Desk's Worktrees tab: one Hermes worktree each for orders-api, web-app and orders-types under ~/code, with each worktree's hermes/ branch, task, age and disk size, and the free disk space.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One rule before you read the diff: the agent may not touch the recording. If this prints anything, the test was moved to fit the code, and the spike is void:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--stat&lt;/span&gt; main...hermes/rewrite-shipping-quote &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; tests/golden
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then read the rewrite in the Review Desk (⌘G), turn by turn. Two endings, both cheap. If the golden master is green and the new code is easier to read, Land commits it, opens a pull request or merges locally, and can be undone. If it isn't, close the session. When you close it, you choose whether to keep the worktree, and since 2.2.9 a kept worktree isn't deleted later (&lt;a href="https://github.com/hermes-hq/hermes-ide/releases/tag/v2.2.9" rel="noopener noreferrer"&gt;release notes&lt;/a&gt;). Throw it away, and the old module is exactly where it was.&lt;/p&gt;

&lt;p&gt;Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Anthropic, OpenAI, Nous Research or any agent vendor. If you'd rather stay in plain git, the same flow is &lt;code&gt;git worktree add&lt;/code&gt;, your test command, and &lt;code&gt;git worktree remove&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  So is Spolsky still right?
&lt;/h2&gt;

&lt;p&gt;About whole products, yes, and agents make his warning more urgent. A team can now start a full rewrite in a morning, which means it can throw away ten years of bug fixes before anyone in the room has asked what they were.&lt;/p&gt;

&lt;p&gt;About one module, behind a port, against a golden master recorded from the old code, the rule bends. That isn't a rewrite from scratch in Spolsky's sense. The old knowledge goes with you, as a file of inputs and expected outputs the agent has to satisfy.&lt;/p&gt;

&lt;p&gt;Record that file first, and an agent rewrite is a spike you can afford to lose.&lt;/p&gt;

&lt;p&gt;Has an agent "cleaned up" an edge case out of your code yet? How did you find out? Tell me in the comments.&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;Putting the old module and the rewrite behind one port is what makes the golden master possible, and it's the same move that lets a PHP application outlive its framework. &lt;em&gt;Decoupled PHP&lt;/em&gt; is my book on doing that with clean and hexagonal architecture, one seam at a time instead of one big rewrite. If you try the spike above, &lt;a href="https://hermes-ide.com/download" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; gives each attempt its own branch and worktree, and &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;the repo is on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/dp/B0H1WVFBYY" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2Fcz4ytbajureytql7wwi4.jpg" alt="Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework" width="333" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>php</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Vercel's agent-desktop Lets Coding Agents Click Native Apps Without Moving Your Mouse</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Mon, 02 Nov 2026 02:01:24 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/vercels-agent-desktop-lets-coding-agents-click-native-apps-without-moving-your-mouse-38p1</link>
      <guid>https://dev.to/gabrielanhaia/vercels-agent-desktop-lets-coding-agents-click-native-apps-without-moving-your-mouse-38p1</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GYJZ2XJD" rel="noopener noreferrer"&gt;AI Agents Pocket Guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also by me:&lt;/strong&gt; &lt;em&gt;Thinking in Go&lt;/em&gt; (2-book series) — &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; + &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Your coding agent can now open Finder, read every button in the window as JSON, and click one by its ID, while your own pointer stays exactly where you left it.&lt;/p&gt;

&lt;p&gt;That is the pitch of &lt;a href="https://github.com/vercel-labs/agent-desktop" rel="noopener noreferrer"&gt;agent-desktop&lt;/a&gt;, which Vercel Labs released as version 1.0.2 &lt;a href="https://github.com/vercel-labs/agent-desktop/releases" rel="noopener noreferrer"&gt;on October 30&lt;/a&gt;. The &lt;a href="https://github.com/vercel-labs/agent-desktop" rel="noopener noreferrer"&gt;README&lt;/a&gt; calls it "Computer use for AI agents: read and operate desktop apps through the OS accessibility tree." It is a Rust CLI plus a C library, Apache-2.0, and it runs on macOS, Windows and Linux. It contains no model. It is pitched as computer use for &lt;a href="https://x.com/ctatedev/status/2116323148706771302" rel="noopener noreferrer"&gt;any coding agent&lt;/a&gt;, and the agent drives it by running shell commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped, and when
&lt;/h2&gt;

&lt;p&gt;The builder's launch post went up on X at 23:48 UTC on October 30, &lt;a href="https://cellcog.ai/blog/vercel-agent-desktop/" rel="noopener noreferrer"&gt;according to CellCog&lt;/a&gt;. Vercel's &lt;a href="https://github.com/ctate" rel="noopener noreferrer"&gt;Chris Tate&lt;/a&gt; then &lt;a href="https://x.com/ctatedev/status/2116323148706771302" rel="noopener noreferrer"&gt;described it&lt;/a&gt; as "agent-browser, for your whole computer," a nod to Vercel's &lt;a href="https://github.com/vercel-labs/agent-browser" rel="noopener noreferrer"&gt;agent-browser&lt;/a&gt;, which does the same thing for web pages. Vercel CEO Guillermo Rauch &lt;a href="https://x.com/rauchg/status/2116345666108854781" rel="noopener noreferrer"&gt;shared it&lt;/a&gt; with "An agent on every desktop."&lt;/p&gt;

&lt;p&gt;The project is older than the launch: it started in February and later moved under the Vercel Labs organization, &lt;a href="https://shiftdelete.net/en/vercel-agent-desktop-en" rel="noopener noreferrer"&gt;per ShiftDelete&lt;/a&gt;. Windows 10 1809+ support arrived in &lt;a href="https://github.com/vercel-labs/agent-desktop/releases" rel="noopener noreferrer"&gt;v0.10.0 on October 18&lt;/a&gt;, and v1.0.2 on October 30 added a &lt;a href="https://agent-browser.dev/desktop" rel="noopener noreferrer"&gt;docs site&lt;/a&gt; and made &lt;code&gt;AGENTS.md&lt;/code&gt; the single instructions file for agents. &lt;a href="https://www.marktechpost.com/2026/10/31/vercel-labs-agent-desktop-computer-use/" rel="noopener noreferrer"&gt;MarkTechPost&lt;/a&gt; and &lt;a href="https://www.alextech.ai/en/news/vercel-open-sources-agent-desktop-to-use-pc-apps-without-screenshots/" rel="noopener noreferrer"&gt;AlexTech&lt;/a&gt; covered it the next day. The repo had about 6,900 stars on November 2, &lt;a href="https://github.com/vercel-labs/agent-desktop" rel="noopener noreferrer"&gt;per its GitHub page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://agent-browser.dev/desktop" rel="noopener noreferrer"&gt;docs&lt;/a&gt; list the floors: macOS 13 or later (Intel or Apple Silicon) through the AX API, Windows 10 1809 or later through UI Automation, and Ubuntu 24.04 or later through AT-SPI, on x64 and ARM64.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the loop works
&lt;/h2&gt;

&lt;p&gt;The tool does one step per call. Each command prints one &lt;a href="https://agent-browser.dev/desktop" rel="noopener noreferrer"&gt;JSON envelope&lt;/a&gt; and exits. The observe, act, verify loop lives in your agent. Adapted from the &lt;a href="https://github.com/vercel-labs/agent-desktop" rel="noopener noreferrer"&gt;README&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# setup&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; agent-desktop
agent-desktop status
agent-desktop skills get desktop

&lt;span class="c"&gt;# look: interactive elements, each with a ref&lt;/span&gt;
agent-desktop snapshot &lt;span class="nt"&gt;--app&lt;/span&gt; Finder &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;span class="c"&gt;# act on a ref from that snapshot&lt;/span&gt;
agent-desktop click @s8f3k2p9:e8
&lt;span class="c"&gt;# look again to check it worked&lt;/span&gt;
agent-desktop snapshot &lt;span class="nt"&gt;--app&lt;/span&gt; Finder &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ref values above are the README's examples. Yours come from your own snapshot. Refs are tied to the snapshot that produced them: v0.10.0 &lt;a href="https://github.com/vercel-labs/agent-desktop/releases" rel="noopener noreferrer"&gt;removed bare refs&lt;/a&gt; like &lt;code&gt;@e8&lt;/code&gt;, and the README says they now fail with &lt;code&gt;INVALID_ARGS&lt;/code&gt;. If the UI changed, the call returns &lt;code&gt;STALE_REF&lt;/code&gt; and the agent should snapshot again. If several windows match, &lt;code&gt;AMBIGUOUS_TARGET&lt;/code&gt; lists candidate window IDs to pass with &lt;code&gt;--window-id&lt;/code&gt;. Every error carries a &lt;code&gt;disposition&lt;/code&gt;, and the &lt;a href="https://github.com/vercel-labs/agent-desktop" rel="noopener noreferrer"&gt;README&lt;/a&gt; says to retry only when &lt;code&gt;error.disposition.retry&lt;/code&gt; is &lt;code&gt;safe&lt;/code&gt;. Put that rule in your agent's &lt;code&gt;AGENTS.md&lt;/code&gt; or system prompt.&lt;/p&gt;

&lt;p&gt;Calls are headless by default. Per the &lt;a href="https://github.com/vercel-labs/agent-desktop" rel="noopener noreferrer"&gt;README&lt;/a&gt;, ref actions go through the accessibility API "without moving the pointer or taking focus." A global &lt;code&gt;--headed&lt;/code&gt; flag allows physical mouse and keyboard input when an app needs it. With several agents at once, each subagent gets its own colored overlay cursor, &lt;a href="https://shiftdelete.net/en/vercel-agent-desktop-en" rel="noopener noreferrer"&gt;ShiftDelete reports&lt;/a&gt;, and actions that need the one physical pointer take turns.&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%2Fhywt6wdubel21ln38bd7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhywt6wdubel21ln38bd7.png" alt="An editorial illustration of a branching tree of interface nodes rising out of a desktop window, a small robot hand tapping one glowing orange node while a real mouse pointer rests untouched to the side" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The agent reads the app's accessibility tree and acts on one node by ref. Your pointer isn't part of it.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The token numbers are the builder's own
&lt;/h2&gt;

&lt;p&gt;The project's headline argument against screenshot-based computer use is cost. Every figure for that so far comes from the project itself.&lt;/p&gt;

&lt;p&gt;The builder's posts report 273 tokens per look at Apple Maps when you snapshot one region, against 3,811 for the whole window, and a simulated 52-step task dropping from 198k tokens to 31k, &lt;a href="https://cellcog.ai/blog/vercel-agent-desktop/" rel="noopener noreferrer"&gt;as summarised by CellCog&lt;/a&gt;. &lt;a href="https://www.alextech.ai/en/news/vercel-open-sources-agent-desktop-to-use-pc-apps-without-screenshots/" rel="noopener noreferrer"&gt;AlexTech&lt;/a&gt; puts the skeleton mode's saving at 78% to 96% against full-tree dumps in Slack, VS Code and Notion, and calls these "self-reported figures not verified by third parties." CellCog also points out that there is &lt;a href="https://cellcog.ai/blog/vercel-agent-desktop/" rel="noopener noreferrer"&gt;no shared yardstick&lt;/a&gt; for these numbers yet.&lt;/p&gt;

&lt;p&gt;The technique behind the saving is in the README. You map a dense app shallowly first, then drill into the part you care about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-desktop snapshot &lt;span class="nt"&gt;--skeleton&lt;/span&gt; &lt;span class="nt"&gt;--app&lt;/span&gt; Slack &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--compact&lt;/span&gt;
agent-desktop snapshot &lt;span class="nt"&gt;--root&lt;/span&gt; @s8f3k2p9:e3 &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--compact&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I haven't run agent-desktop myself, so I can only relay these numbers. If you try it, count tokens on your own app. Slack and Notion are not your internal Electron tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can't see
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://agent-browser.dev/desktop" rel="noopener noreferrer"&gt;docs&lt;/a&gt; are blunt about the limits. It reads accessibility trees and does not match pixels, so a UI that publishes no accessibility tree is invisible to it. &lt;a href="https://www.alextech.ai/en/news/vercel-open-sources-agent-desktop-to-use-pc-apps-without-screenshots/" rel="noopener noreferrer"&gt;How well it works&lt;/a&gt; depends on how well each app's interface is built.&lt;/p&gt;

&lt;p&gt;It also does not automate web pages. For those, use agent-browser. For web content inside a Chromium or Electron app, agent-desktop opens a &lt;a href="https://agent-browser.dev/desktop" rel="noopener noreferrer"&gt;DevTools port&lt;/a&gt; that agent-browser can connect to.&lt;/p&gt;

&lt;p&gt;The CLI has &lt;a href="https://agent-browser.dev/desktop" rel="noopener noreferrer"&gt;60 command names&lt;/a&gt;, the same on every platform. Four of them (&lt;code&gt;key-down&lt;/code&gt;, &lt;code&gt;key-up&lt;/code&gt;, &lt;code&gt;mouse-down&lt;/code&gt;, &lt;code&gt;mouse-up&lt;/code&gt;) fail closed everywhere. And a successful action does not prove the UI changed, which is why the docs recommend a follow-up read such as &lt;code&gt;wait --element ... --predicate value&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizbbm7g2ei09rare7q2s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizbbm7g2ei09rare7q2s.png" alt="An editorial illustration of a magnifying glass over a desktop window split in two: the left half shows a neat tree of nodes, the right half is a blank black panel with no structure for the lens to find" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;No accessibility tree, no view. Pixel-only UIs stay out of reach.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you hand it to an agent
&lt;/h2&gt;

&lt;p&gt;A few things in the docs are worth settling before the first run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who gets the permission.&lt;/strong&gt; On macOS the &lt;a href="https://agent-browser.dev/desktop" rel="noopener noreferrer"&gt;Accessibility permission&lt;/a&gt; goes to the app that runs agent-desktop. If your agent runs in a terminal, that terminal is the app asking, so decide on purpose which one gets it. &lt;code&gt;agent-desktop permissions --guide&lt;/code&gt; walks you through it, and the &lt;a href="https://github.com/vercel-labs/agent-desktop" rel="noopener noreferrer"&gt;README&lt;/a&gt; also requires &lt;code&gt;TMPDIR&lt;/code&gt; to be owned by you with mode &lt;code&gt;0700&lt;/code&gt;. On Windows no grant is needed for apps at your terminal's integrity level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What ends up in transcripts.&lt;/strong&gt; The &lt;a href="https://github.com/vercel-labs/agent-desktop" rel="noopener noreferrer"&gt;README&lt;/a&gt; warns that "Snapshots, clipboard reads, screenshots, command output and debug artifacts can contain private desktop content." The &lt;a href="https://github.com/vercel-labs/agent-desktop/blob/main/docs/faq.md" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt; goes further: "redaction does not make a screenshot safe to publish." Your agent's transcript now holds whatever was on screen, so treat it like a log with secrets in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What leaves the machine.&lt;/strong&gt; The &lt;a href="https://github.com/vercel-labs/agent-desktop/blob/main/docs/faq.md" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt; says the native CLI "does not require a remote model service." The optional JEV helper is different: it sends goals and screen descriptions to the model provider you configure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What git won't show you.&lt;/strong&gt; A click in Finder leaves no diff. Keep desktop actions on a short leash: name the app, name the task, and use &lt;code&gt;--headed&lt;/code&gt; only when headless fails. I build &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt;, so weigh this with that in mind. Hermes runs each agent in a real terminal, so an agent there can call agent-desktop like any other CLI. But Hermes, by its &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;README&lt;/a&gt;, "observes what agents report on purpose and what git shows." Its Review Desk can show you every file an agent changed, turn by turn. It cannot show you what the agent clicked in another app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would you give it your desktop?
&lt;/h2&gt;

&lt;p&gt;A cheap first test: point your agent at one app, let it run &lt;code&gt;snapshot&lt;/code&gt; only, and read what comes back before it gets to click anything.&lt;/p&gt;

&lt;p&gt;Which native app would you trust an agent to drive first, and which one would you never hand over? Tell me in the comments.&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;agent-desktop is a tool, and the interesting design work sits in the loop around it: when to look again, when a retry is safe, when to stop and ask. That loop is what the &lt;a href="https://www.amazon.com/dp/B0GYJZ2XJD" rel="noopener noreferrer"&gt;AI Agents Pocket Guide&lt;/a&gt; is about, from tool calling to guardrails for agents that act on their own.&lt;/p&gt;

&lt;p&gt;If you run more than one coding agent, &lt;a href="https://hermes-ide.com/download" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; is free, source-available and on macOS, Windows and Linux, and the code is on &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/dp/B0GYJZ2XJD" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2Ffzkyb8bqzkmlt4qzgqbo.jpg" alt="AI Agents Pocket Guide: Patterns for Building Autonomous Systems with LLMs" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your Agent Never Committed. Snapshot Every Turn to a Git Ref, Then Bisect</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Mon, 02 Nov 2026 01:01:06 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/your-agent-never-committed-snapshot-every-turn-to-a-git-ref-then-bisect-5hc9</link>
      <guid>https://dev.to/gabrielanhaia/your-agent-never-committed-snapshot-every-turn-to-a-git-ref-then-bisect-5hc9</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GX3B8371" rel="noopener noreferrer"&gt;Event-Driven Architecture Pocket Guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The series:&lt;/strong&gt; &lt;em&gt;Pocket Guides for Developers&lt;/em&gt; — 8 short books on agents, RAG, prompts, observability, events, databases and system design — &lt;a href="https://www.amazon.com/dp/B0GYLH38X4" rel="noopener noreferrer"&gt;the whole series&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Turn 20. The agent says the refund endpoint is done. You run the suite and &lt;code&gt;test/refund.test.ts&lt;/code&gt; fails on a case that passed an hour ago. Somewhere between turn 1 and turn 20, something broke.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git log&lt;/code&gt; shows the commit you started from and nothing after it. The agent never committed, so twenty turns of edits sit in your working tree as one diff. &lt;a href="https://git-scm.com/docs/git-bisect" rel="noopener noreferrer"&gt;git bisect&lt;/a&gt; is the tool for "which change broke this", and it needs commits to walk. You have none.&lt;/p&gt;

&lt;p&gt;Fix it for the next session. A &lt;a href="https://code.claude.com/docs/en/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; Stop hook writes one commit per turn to a private ref with git plumbing, so your branch, your staging area and your stash never move. A short script then bisects those snapshots in a scratch worktree while your checkout stays where it is. &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt;, which I build, does the snapshot half without the hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why /rewind and git stash create both fall short
&lt;/h2&gt;

&lt;p&gt;Claude Code already has &lt;a href="https://code.claude.com/docs/en/checkpointing" rel="noopener noreferrer"&gt;checkpoints&lt;/a&gt;. It captures your code before each prompt that starts a turn, keeps file snapshots for the 100 most recent checkpoints in a session, and &lt;code&gt;/rewind&lt;/code&gt; (or Esc twice on an empty prompt) restores code, conversation or both. For "undo the last thing", that is the right tool.&lt;/p&gt;

&lt;p&gt;The docs say checkpointing does not track files changed by Bash commands, so an &lt;code&gt;rm&lt;/code&gt;, an &lt;code&gt;mv&lt;/code&gt;, a formatter or a codegen script run by the agent is invisible to it. Restoring also changes your working tree: you can't run the test against turn 7 while keeping turn 20 on disk. The same page says checkpoints are not a replacement for version control.&lt;/p&gt;

&lt;p&gt;The obvious git answer is &lt;a href="https://git-scm.com/docs/git-stash" rel="noopener noreferrer"&gt;git stash create&lt;/a&gt;. It builds a stash commit and prints its id "without storing it anywhere in the ref namespace". Your index and files stay as they are. It still falls short here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It takes only a message. The &lt;code&gt;-u&lt;/code&gt; option that pulls in untracked files belongs to &lt;code&gt;push&lt;/code&gt; and &lt;code&gt;save&lt;/code&gt;, so every file the agent created from scratch is missing from the snapshot.&lt;/li&gt;
&lt;li&gt;A stash commit's first parent is the commit at &lt;code&gt;HEAD&lt;/code&gt;. Twenty snapshots would be twenty siblings hanging off the same commit, and bisect needs a line from good to bad.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So build the commit by hand with the same plumbing, a temporary index, and the previous snapshot as the parent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: a Stop hook that commits every turn to a private ref
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;.claude/hooks/snap.sh&lt;/code&gt;. It needs &lt;a href="https://jqlang.org" rel="noopener noreferrer"&gt;jq&lt;/a&gt; to read the hook input.&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;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# .claude/hooks/snap.sh: snapshot the tree after each turn.&lt;/span&gt;
&lt;span class="c"&gt;# Always exits 0. Exit 2 would keep Claude from stopping.&lt;/span&gt;
&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;sid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .session_id&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .cwd&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"refs/agent-turns/&lt;/span&gt;&lt;span class="nv"&gt;$sid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--git-path&lt;/span&gt; index&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$idx&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nv"&gt;tree&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;GIT_INDEX_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$idx&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s1"&gt;'git add -A &amp;amp;&amp;amp; git write-tree'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$idx&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tree&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="nv"&gt;prev&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--verify&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ref&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nv"&gt;prev&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tree&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$prev&lt;/span&gt;&lt;span class="s2"&gt;^{tree}"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git commit-tree &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tree&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$prev&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"agent turn &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
git update-ref &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ref&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script copies your real index to a temp file and points &lt;a href="https://git-scm.com/docs/git" rel="noopener noreferrer"&gt;&lt;code&gt;GIT_INDEX_FILE&lt;/code&gt;&lt;/a&gt; at it, so &lt;code&gt;git add -A&lt;/code&gt; starts from your current index and your real staging area is never written. &lt;a href="https://git-scm.com/docs/git-rev-parse" rel="noopener noreferrer"&gt;&lt;code&gt;git rev-parse --git-path index&lt;/code&gt;&lt;/a&gt; finds that real index, including inside a linked worktree, and it runs before &lt;code&gt;GIT_INDEX_FILE&lt;/code&gt; is set because &lt;code&gt;--git-path&lt;/code&gt; takes that variable into account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://git-scm.com/docs/git-add" rel="noopener noreferrer"&gt;&lt;code&gt;git add -A&lt;/code&gt;&lt;/a&gt; stages new, modified and deleted files and skips anything in &lt;code&gt;.gitignore&lt;/code&gt;, so a &lt;code&gt;.env&lt;/code&gt; or &lt;code&gt;node_modules&lt;/code&gt; you already ignore stays out of the snapshots. Anything untracked and not ignored goes in, and it stays in the snapshot history after you delete the file. Check your &lt;code&gt;.gitignore&lt;/code&gt; before you turn the hook on.&lt;/p&gt;

&lt;p&gt;Then the tree becomes history:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/docs/git-commit-tree" rel="noopener noreferrer"&gt;&lt;code&gt;git commit-tree&lt;/code&gt;&lt;/a&gt; turns that tree into a commit whose parent (&lt;code&gt;-p&lt;/code&gt;) is the previous snapshot. The first snapshot of a session hangs off &lt;code&gt;HEAD&lt;/code&gt;. That chain is what makes bisect possible.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/docs/git-update-ref" rel="noopener noreferrer"&gt;&lt;code&gt;git update-ref&lt;/code&gt;&lt;/a&gt; moves &lt;code&gt;refs/agent-turns/&amp;lt;session-id&amp;gt;&lt;/code&gt; to the new commit. A turn that changed nothing produces no snapshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Register it in &lt;code&gt;.claude/settings.json&lt;/code&gt;:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&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="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"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;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${CLAUDE_PROJECT_DIR}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;/.claude/hooks/snap.sh"&lt;/span&gt;&lt;span class="w"&gt;
          &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="w"&gt;
      &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="w"&gt;
  &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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;chmod +x .claude/hooks/snap.sh&lt;/code&gt;. Per the &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks reference&lt;/a&gt;, Stop fires when Claude finishes responding, takes no matcher, and gets JSON on stdin with &lt;code&gt;session_id&lt;/code&gt; and &lt;code&gt;cwd&lt;/code&gt; among other fields. It also asks you to wrap the placeholder in double quotes in a shell-form command, which keeps a project path with a space working. The rest of the script follows from that page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exit code 2 from a Stop hook blocks the stop and keeps Claude going. A snapshot script must never do that, hence &lt;code&gt;exit 0&lt;/code&gt; on every path.&lt;/li&gt;
&lt;li&gt;Stop does not run when you interrupt the turn. Those edits are not lost: they land in the next turn's snapshot.&lt;/li&gt;
&lt;li&gt;If Claude enters a worktree, &lt;code&gt;$CLAUDE_PROJECT_DIR&lt;/code&gt; still points at the original project root while &lt;code&gt;cwd&lt;/code&gt; follows the worktree. That is why the script &lt;code&gt;cd&lt;/code&gt;s to &lt;code&gt;cwd&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The refs live under &lt;code&gt;refs/&lt;/code&gt;, and git's &lt;a href="https://git-scm.com/docs/git-worktree" rel="noopener noreferrer"&gt;worktree docs&lt;/a&gt; say refs there are shared by every worktree of the repository. Step 3 depends on that. By default &lt;a href="https://git-scm.com/docs/git-push" rel="noopener noreferrer"&gt;&lt;code&gt;git push&lt;/code&gt;&lt;/a&gt; sends only the current branch, so these refs stay on your machine unless you push them by name, use &lt;code&gt;--mirror&lt;/code&gt;, or set a &lt;code&gt;remote.&amp;lt;name&amp;gt;.push&lt;/code&gt; refspec that covers them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: read the turns
&lt;/h2&gt;

&lt;p&gt;Each session gets one ref. List them, then walk one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="nt"&gt;-each-ref&lt;/span&gt; refs/agent-turns/
&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;refs/agent-turns/&amp;lt;session-id&amp;gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ref&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--not&lt;/span&gt; HEAD   &lt;span class="c"&gt;# one line per turn&lt;/span&gt;
git log &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ref&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--not&lt;/span&gt; HEAD          &lt;span class="c"&gt;# every turn's diff&lt;/span&gt;
git diff &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ref&lt;/span&gt;&lt;span class="s2"&gt;~1"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ref&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;              &lt;span class="c"&gt;# the last turn only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--not HEAD&lt;/code&gt; stops the log at the commit the session started from, so you see the agent's turns and nothing older. Because each snapshot's parent is the previous one, &lt;code&gt;git show &amp;lt;snapshot&amp;gt;&lt;/code&gt; is exactly what that one turn changed, including files a shell command created or deleted.&lt;/p&gt;

&lt;p&gt;To pull one file back from before a given turn without touching anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git restore &lt;span class="nt"&gt;--source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;snapshot&amp;gt;~1 &lt;span class="nt"&gt;--&lt;/span&gt; src/refund.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: bisect the snapshots in a scratch worktree
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;find-bad-turn.sh&lt;/code&gt; somewhere outside the repo:&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;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# find-bad-turn.sh &amp;lt;session-id&amp;gt; &amp;lt;test command...&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;
&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"refs/agent-turns/&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift
&lt;/span&gt;&lt;span class="nv"&gt;good&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GOOD&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
git worktree add &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ref&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git bisect start &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ref&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$good&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git bisect run &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;git bisect reset
&lt;span class="nb"&gt;cd&lt;/span&gt; - &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
git worktree remove &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it from your repo with the failing test as the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/bin/find-bad-turn.sh &amp;lt;session-id&amp;gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'npm ci --silent || exit 125; npm test'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scratch worktree is a throwaway folder with a detached &lt;code&gt;HEAD&lt;/code&gt;, the case &lt;a href="https://git-scm.com/docs/git-worktree" rel="noopener noreferrer"&gt;&lt;code&gt;git worktree add --detach&lt;/code&gt;&lt;/a&gt; is meant for. Bisect state lives in &lt;code&gt;refs/bisect&lt;/code&gt;, which the same docs list as one of the exceptions that are not shared between worktrees, so your own checkout never sees a bisect in progress. &lt;code&gt;GOOD&lt;/code&gt; defaults to &lt;code&gt;HEAD&lt;/code&gt;, the commit the session started from. If you committed during the session, pass the right base with &lt;code&gt;GOOD=&amp;lt;sha&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The test command follows the &lt;a href="https://git-scm.com/docs/git-bisect" rel="noopener noreferrer"&gt;&lt;code&gt;git bisect run&lt;/code&gt;&lt;/a&gt; contract: exit 0 means good, 1 to 127 (except 125) means bad, and 125 means "can't test this one, skip it". Snapshots leave ignored files out, so the worktree has no &lt;code&gt;node_modules&lt;/code&gt;. That is why the command installs first and turns a failed install into 125 instead of a false "bad".&lt;/p&gt;

&lt;p&gt;Bisect halves the range each step. Twenty turns take about five test runs (log2 of 20 is about 4.3), and it prints the first bad snapshot. Its commit message carries the turn's UTC timestamp, so you can match it to the conversation.&lt;/p&gt;

&lt;p&gt;Bisect assumes that once a turn breaks the test, every later turn is broken too. Agents don't always behave that way: turn 9 breaks it, turn 12 fixes it, turn 17 breaks it again. If the result looks odd, scan in order instead, inside the same kind of scratch worktree, with &lt;code&gt;ref&lt;/code&gt; and &lt;code&gt;good&lt;/code&gt; set as in the script:&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="k"&gt;for &lt;/span&gt;c &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;git rev-list &lt;span class="nt"&gt;--reverse&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ref&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--not&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$good&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;do
  &lt;/span&gt;git checkout &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  npm ci &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"first bad: &lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have the bad snapshot, undo only that turn in your real working tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--binary&lt;/span&gt; &amp;lt;bad&amp;gt;~1 &amp;lt;bad&amp;gt; | git apply &lt;span class="nt"&gt;-R&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a later turn edited the same lines, &lt;code&gt;git apply&lt;/code&gt; refuses and tells you which hunk, which is the conflict you want to see before anything is overwritten. When the session is merged and you no longer need its history, delete the refs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="nt"&gt;-each-ref&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'delete %(refname)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  refs/agent-turns/ | git update-ref &lt;span class="nt"&gt;--stdin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If bisect is new to you, I walked through &lt;code&gt;git bisect run&lt;/code&gt; end to end in &lt;a href="https://dev.to/gabrielanhaia/let-claude-code-run-git-bisect-in-its-own-worktree-while-you-keep-coding-1g91"&gt;Let Claude Code Run git bisect in Its Own Worktree While You Keep Coding&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Hermes IDE does the snapshot part for you
&lt;/h2&gt;

&lt;p&gt;I build Hermes IDE, so weigh this section with that in mind. The hook above is Claude Code only. Other coding agents each have their own hook system, or none, so you would write and maintain a version per agent.&lt;/p&gt;

&lt;p&gt;Hermes saves every agent turn, and you can diff or restore any single turn, with undo (&lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;README&lt;/a&gt;). Under each session, the Agent replies strip shows one chip per reply with the lines it changed. Click a chip to see its changes or restore your files to that point; a "+N earlier" chip opens every reply with its time, files and lines changed (&lt;a href="https://hermes-ide.com/changelog" rel="noopener noreferrer"&gt;changelog, 2.3.1&lt;/a&gt;). Turn history snapshots from tasks that ran in their own worktree are removed from the repository 14 days after the session closes (&lt;a href="https://github.com/hermes-hq/hermes-ide/releases/tag/v2.2.9" rel="noopener noreferrer"&gt;v2.2.9 release notes&lt;/a&gt;), so that history cleans itself up.&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%2F8cgisbligwk7mx2lpa8d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8cgisbligwk7mx2lpa8d.png" alt="Hermes IDE Review Desk in By turn view for a Claude Code task, listing turns T1 and T2 with turn 1's diff of src/db.ts" width="800" height="444"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Review Desk (⌘G) in By turn view for a Claude Code task in orders-api: T1 (3 files, +69 −1) and T2 (1 file, +1 −0), turn 1's diff of src/db.ts adding a refunded status, the changed files, and a commit message drafted from the turns.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;⌘G (Ctrl+Shift+G on Windows and Linux) opens the Review Desk, which shows changes by turn or by file with risk flags, and sends a line comment back to the agent that wrote the line. Hermes doesn't bisect for you: the README and recent release notes describe reading and restoring turns, not running a test across them. For "which of twenty turns broke this", the script above is still the tool. Hermes is where you read that turn and roll it back without touching the others.&lt;/p&gt;

&lt;p&gt;Hermes is free to download, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It is not affiliated with Nous Research, Anthropic, OpenAI or any agent vendor. &lt;a href="https://hermes-ide.com/download" rel="noopener noreferrer"&gt;Download it&lt;/a&gt;, and if it saves you a bisect, &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;star it on GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;A chain of snapshots you replay until the state goes wrong is an append-only history, and that idea sits at the heart of event-driven systems too. The &lt;a href="https://www.amazon.com/dp/B0GX3B8371" rel="noopener noreferrer"&gt;Event-Driven Architecture Pocket Guide&lt;/a&gt; covers that side at system scale: sagas, CQRS, the outbox, and the traps nobody warns you about. It is one of 8 short books in &lt;em&gt;Pocket Guides for Developers&lt;/em&gt;, on agents, RAG, prompts, observability, events, databases and system design: &lt;a href="https://www.amazon.com/dp/B0GYLH38X4" rel="noopener noreferrer"&gt;the whole series is here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/dp/B0GX3B8371" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2Fblk678n27i2r4chdp8zh.jpg" alt="Event-Driven Architecture Pocket Guide: Saga, CQRS, Outbox, and the Traps Nobody Warns You About" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>ai</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your Agent's ADD FOREIGN KEY Stalls Writes on Two Tables. NOT VALID, Then VALIDATE</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Mon, 02 Nov 2026 00:01:27 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/your-agents-add-foreign-key-stalls-writes-on-two-tables-not-valid-then-validate-586e</link>
      <guid>https://dev.to/gabrielanhaia/your-agents-add-foreign-key-stalls-writes-on-two-tables-not-valid-then-validate-586e</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GYLMVX9S" rel="noopener noreferrer"&gt;Database Playbook&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also by me:&lt;/strong&gt; &lt;em&gt;Thinking in Go&lt;/em&gt; (2-book series) — &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; + &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Checkout writes hang in the middle of a deploy. The only change in that deploy is the migration your coding agent wrote when you asked it to link orders to customers, and it looked textbook: &lt;code&gt;ALTER TABLE orders ADD FOREIGN KEY (customer_id) REFERENCES customers (id);&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Per the &lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html" rel="noopener noreferrer"&gt;PostgreSQL 18 &lt;code&gt;ALTER TABLE&lt;/code&gt; docs&lt;/a&gt;, &lt;code&gt;ADD FOREIGN KEY&lt;/code&gt; takes a &lt;code&gt;SHARE ROW EXCLUSIVE&lt;/code&gt; lock on &lt;code&gt;orders&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; on &lt;code&gt;customers&lt;/code&gt;, then scans the table to check every existing row. The same page warns that while that scan runs, "other updates to the table are locked out until the &lt;code&gt;ALTER TABLE ADD CONSTRAINT&lt;/code&gt; command is committed." On a big &lt;code&gt;orders&lt;/code&gt; table, that is every checkout and every customer-profile edit waiting on one migration.&lt;/p&gt;

&lt;p&gt;The fix is two statements and an index the agent also skipped, and you can watch the stall happen on your laptop first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one line locks two tables
&lt;/h2&gt;

&lt;p&gt;Postgres table locks come in eight modes, and the &lt;a href="https://www.postgresql.org/docs/current/explicit-locking.html" rel="noopener noreferrer"&gt;explicit locking chapter&lt;/a&gt; lists which ones conflict. Three matter here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; and &lt;code&gt;MERGE&lt;/code&gt; take &lt;code&gt;ROW EXCLUSIVE&lt;/code&gt; on their target table. &lt;code&gt;SHARE ROW EXCLUSIVE&lt;/code&gt; conflicts with &lt;code&gt;ROW EXCLUSIVE&lt;/code&gt;; the docs describe it as a mode that "protects a table against concurrent data changes." So while the foreign-key migration holds it, writes to both tables queue.&lt;/p&gt;

&lt;p&gt;Reads are fine. Plain &lt;code&gt;SELECT&lt;/code&gt; takes &lt;code&gt;ACCESS SHARE&lt;/code&gt;, which conflicts with &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; only. That is why this failure is sneaky: the dashboards that read keep rendering while every write to those two tables hangs.&lt;/p&gt;

&lt;p&gt;And the lock does not end when the scan ends. Per the same chapter, "once acquired, a lock is normally held until the end of the transaction." If your migration runner wraps each migration in a transaction, and many do, the window is the scan plus everything else in that migration file.&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%2Fjn345d4ycjf20o6cl8og.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjn345d4ycjf20o6cl8og.png" alt="Two database tables chained together by one padlock while a queue of writers waits outside" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One ALTER statement, two tables locked against writes until it commits.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Reproduce the stall in Docker
&lt;/h2&gt;

&lt;p&gt;Start a throwaway Postgres 18 from the &lt;a href="https://hub.docker.com/_/postgres" rel="noopener noreferrer"&gt;official image&lt;/a&gt; (&lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; is required) and open a &lt;code&gt;psql&lt;/code&gt; shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--name&lt;/span&gt; fk-lab &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; postgres:18
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; fk-lab psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the two tables with no relationship yet: 100,000 customers and a million orders that all point at real customers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt;
              &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total_cents&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'customer '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&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="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;99999&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open three more terminals (four sessions in all), each running the same &lt;code&gt;docker exec -it fk-lab psql -U postgres&lt;/code&gt;. In &lt;strong&gt;session 1&lt;/strong&gt;, run the agent's migration inside a transaction and leave it open. That mimics a migration runner that has more statements to go, and it keeps the lock around long enough to look at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- do not COMMIT yet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;strong&gt;session 2&lt;/strong&gt;, place an order. In &lt;strong&gt;session 3&lt;/strong&gt;, edit a customer. Both statements hang:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- session 2&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1999&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- session 3&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'renamed'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plain &lt;code&gt;SELECT&lt;/code&gt; against either table, say &lt;code&gt;SELECT count(*) FROM orders;&lt;/code&gt;, still returns, for the &lt;code&gt;ACCESS SHARE&lt;/code&gt; reason above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the blocked writers
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;session 4&lt;/strong&gt;, ask &lt;a href="https://www.postgresql.org/docs/current/monitoring-stats.html" rel="noopener noreferrer"&gt;&lt;code&gt;pg_stat_activity&lt;/code&gt;&lt;/a&gt; who is waiting and &lt;a href="https://www.postgresql.org/docs/current/functions-info.html" rel="noopener noreferrer"&gt;&lt;code&gt;pg_blocking_pids()&lt;/code&gt;&lt;/a&gt; who they are waiting on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;wait_event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;wait_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;pg_blocking_pids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocked_by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;wait_event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Lock'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sessions 2 and 3 should show up with &lt;code&gt;wait_event_type = 'Lock'&lt;/code&gt; and &lt;code&gt;wait_event = 'relation'&lt;/code&gt;, which the monitoring docs define as "waiting to acquire a lock on a relation." Both &lt;code&gt;blocked_by&lt;/code&gt; arrays should hold session 1's pid.&lt;/p&gt;

&lt;p&gt;Then look at the lock table itself with &lt;a href="https://www.postgresql.org/docs/current/view-pg-locks.html" rel="noopener noreferrer"&gt;&lt;code&gt;pg_locks&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;granted&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_locks&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;locktype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'relation'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;relation&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="s1"&gt;'customers'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;granted&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect session 1 holding &lt;code&gt;ShareRowExclusiveLock&lt;/code&gt; with &lt;code&gt;granted = true&lt;/code&gt; on both tables, and sessions 2 and 3 each asking for &lt;code&gt;RowExclusiveLock&lt;/code&gt; with &lt;code&gt;granted = false&lt;/code&gt;. Those are the two halves of the conflict table, live.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;ROLLBACK;&lt;/code&gt; in session 1. Sessions 2 and 3 finish right away.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pg_blocking_pids()&lt;/code&gt; also reports "soft" blocks: a session waiting for a lock that would conflict with yours, and that is ahead of you in the queue. So a foreign-key migration that is itself &lt;em&gt;waiting&lt;/em&gt; behind one long-running write transaction can stall every new writer that arrives after it. &lt;code&gt;lock_timeout&lt;/code&gt; in migration 2 is the guard against that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safe way: index, NOT VALID, VALIDATE
&lt;/h2&gt;

&lt;p&gt;Three migrations instead of one. Run them in this order, each in its own migration file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration 1: the index the agent forgot.&lt;/strong&gt; The &lt;a href="https://www.postgresql.org/docs/current/ddl-constraints.html" rel="noopener noreferrer"&gt;constraints chapter&lt;/a&gt; notes that "the declaration of a foreign key constraint does not automatically create an index on the referencing columns." Without one, every &lt;code&gt;DELETE&lt;/code&gt; from &lt;code&gt;customers&lt;/code&gt;, and every update of &lt;code&gt;customers.id&lt;/code&gt;, has to scan &lt;code&gt;orders&lt;/code&gt; for matching rows. Build it without blocking writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;orders_customer_id_idx&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-createindex.html" rel="noopener noreferrer"&gt;&lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt;&lt;/a&gt; cannot run inside a transaction block, so this migration has to opt out of the runner's transaction. If it fails partway, it leaves an index marked &lt;code&gt;INVALID&lt;/code&gt;; the docs recommend dropping it and running the build again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration 2: add the constraint, skip the scan.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'3s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;orders_customer_id_fkey&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;VALID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This still asks for &lt;code&gt;SHARE ROW EXCLUSIVE&lt;/code&gt; on both tables, but &lt;code&gt;NOT VALID&lt;/code&gt; skips "this potentially-lengthy scan," so the lock is held for a moment instead of a full pass over a million rows. New rows are checked from now on. The &lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html" rel="noopener noreferrer"&gt;ALTER TABLE page&lt;/a&gt; says subsequent inserts and updates "fail unless there is a matching row in the referenced table." Try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;999999999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That insert is rejected with a foreign-key violation. Old rows are not trusted yet, and &lt;a href="https://www.postgresql.org/docs/current/catalog-pg-constraint.html" rel="noopener noreferrer"&gt;&lt;code&gt;pg_constraint&lt;/code&gt;&lt;/a&gt; records that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;conname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;convalidated&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_constraint&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;conrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;contype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'f'&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;convalidated&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt; until the next step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/runtime-config-client.html" rel="noopener noreferrer"&gt;&lt;code&gt;lock_timeout&lt;/code&gt;&lt;/a&gt; is there for the queue problem from the last section. It aborts "any statement that waits longer than the specified amount of time while attempting to acquire a lock" on a table, index, row or other object. Its default is &lt;code&gt;0&lt;/code&gt;, which means wait forever. With three seconds, a migration stuck behind a long transaction fails fast and frees the queue, and you retry it. Set it per session, as here; the docs say setting it in &lt;code&gt;postgresql.conf&lt;/code&gt; is not recommended because it would affect all sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration 3: validate, with writes still flowing.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'3s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
  &lt;span class="n"&gt;VALIDATE&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;orders_customer_id_fkey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the full scan, but under weaker locks. Validation takes &lt;code&gt;SHARE UPDATE EXCLUSIVE&lt;/code&gt; on &lt;code&gt;orders&lt;/code&gt; and &lt;code&gt;ROW SHARE&lt;/code&gt; on &lt;code&gt;customers&lt;/code&gt;. Neither conflicts with &lt;code&gt;ROW EXCLUSIVE&lt;/code&gt;, so inserts, updates and deletes on both tables keep going while it reads. It does conflict with other schema changes and with &lt;code&gt;VACUUM&lt;/code&gt; on &lt;code&gt;orders&lt;/code&gt;, which is the trade you want.&lt;/p&gt;

&lt;p&gt;Repeat the four-session test to see it: wrap the &lt;code&gt;VALIDATE&lt;/code&gt; in &lt;code&gt;BEGIN&lt;/code&gt; without committing, then run the session 2 and 3 writes. They complete. The session 4 query finds no one waiting.&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%2Fspi7qcwa3mfgo93t2sp8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspi7qcwa3mfgo93t2sp8.png" alt="A bridge between two tables drawn first as a dotted outline, then filled in solid while traffic keeps crossing" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;NOT VALID draws the outline for new rows; VALIDATE fills it in while writes keep moving.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If an existing order points at a customer that does not exist, &lt;code&gt;VALIDATE&lt;/code&gt; fails and &lt;code&gt;convalidated&lt;/code&gt; stays &lt;code&gt;false&lt;/code&gt;. Find the orphans first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep migrations 2 and 3 in separate transactions. Run both statements in one transaction and the &lt;code&gt;SHARE ROW EXCLUSIVE&lt;/code&gt; from step 2 is still held through the step 3 scan, because locks live until the transaction ends. You would be back to the one-line version with more typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rule to hand your agent
&lt;/h2&gt;

&lt;p&gt;Agents tend to copy the shape they have seen most, and the one-line &lt;code&gt;ADD FOREIGN KEY&lt;/code&gt; is everywhere. Put the safe shape where the agent reads before it writes a migration. In &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;, a file in &lt;code&gt;.claude/rules/&lt;/code&gt; with a &lt;code&gt;paths:&lt;/code&gt; block loads only when the agent works on matching files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db/migrations/**"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Postgres migration rules&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Never add a foreign key to an existing table in
  one statement. Write two migrations:
&lt;span class="p"&gt;  1.&lt;/span&gt; ADD CONSTRAINT ... FOREIGN KEY ... NOT VALID
&lt;span class="p"&gt;  2.&lt;/span&gt; VALIDATE CONSTRAINT ..., in its own transaction
&lt;span class="p"&gt;-&lt;/span&gt; Every new foreign key gets an index on the
  referencing column, built with
  CREATE INDEX CONCURRENTLY in a migration that
  runs outside a transaction.
&lt;span class="p"&gt;-&lt;/span&gt; Every DDL migration starts with
  SET lock_timeout = '3s'. A timeout means retry,
  not raise the timeout.
&lt;span class="p"&gt;-&lt;/span&gt; Before VALIDATE, run an orphan LEFT JOIN query
  and report the count.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same doc page says Claude treats its instruction files "as context, not enforced configuration," so back the rule with a check that fails the build. &lt;a href="https://squawkhq.com/docs/adding-foreign-key-constraint" rel="noopener noreferrer"&gt;Squawk&lt;/a&gt; has a rule named &lt;code&gt;adding-foreign-key-constraint&lt;/code&gt; that flags this pattern and suggests the &lt;code&gt;NOT VALID&lt;/code&gt; then &lt;code&gt;VALIDATE&lt;/code&gt; split. The agent learns the shape from the rule file, and CI catches the times it forgets.&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;Picking the store, shaping the schema and planning how it changes under live traffic are what the &lt;em&gt;Database Playbook&lt;/em&gt; walks through, store by store. If you let an agent write your migrations, it is the background that lets you review those diffs in a minute instead of finding out from the on-call channel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/dp/B0GYLMVX9S" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2Fktttopxmkwrt9qcazhnc.jpg" alt="Database Playbook: Choosing the Right Store for Every System You Build" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>sql</category>
      <category>ai</category>
    </item>
    <item>
      <title>Prefab UI's Python Sandbox Could Read and Write Your Server's Files. Upgrade to 0.21</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 01 Nov 2026 23:01:13 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/prefab-uis-python-sandbox-could-read-and-write-your-servers-files-upgrade-to-021-1em5</link>
      <guid>https://dev.to/gabrielanhaia/prefab-uis-python-sandbox-could-read-and-write-your-servers-files-upgrade-to-021-1em5</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GYMFPTWV" rel="noopener noreferrer"&gt;System Design Pocket Guide: Fundamentals&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The series:&lt;/strong&gt; &lt;em&gt;Pocket Guides for Developers&lt;/em&gt; — 8 short books on agents, RAG, prompts, observability, events, databases and system design — &lt;a href="https://www.amazon.com/dp/B0GYLH38X4" rel="noopener noreferrer"&gt;the whole series&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;You add one line to your MCP server so the model can draw a chart for the user instead of describing it. The model writes a little Python, a sandbox runs it, and a UI comes back. The word "sandbox" is doing a lot of work in that sentence.&lt;/p&gt;

&lt;p&gt;On November 1, &lt;a href="https://research.jfrog.com/vulnerabilities/prefab-ui-is-vulnerable-to-remote-code-execution-via-a-pyodide-js-bridge-to-deno-cve-2026-120338-jfsa-2026-001699278/" rel="noopener noreferrer"&gt;JFrog Security Research&lt;/a&gt; published CVE-2026-120338 against &lt;a href="https://github.com/PrefectHQ/prefab" rel="noopener noreferrer"&gt;Prefab UI&lt;/a&gt;, the generative UI framework from PrefectHQ. The &lt;a href="https://github.com/advisories/GHSA-3g72-rrqc-f76c" rel="noopener noreferrer"&gt;GitHub advisory&lt;/a&gt; rates it CVSS 10.0. In versions 0.14.0 through 0.20.2, a caller who could submit code to the Generative UI sandbox could read and write files on the host.&lt;/p&gt;

&lt;p&gt;The fix already exists. &lt;a href="https://pypi.org/project/prefab-ui/" rel="noopener noreferrer"&gt;prefab-ui 0.21.0&lt;/a&gt; shipped on October 26, six days before the CVE went public, and 0.21.1 followed the same day. If you run FastMCP with Generative UI turned on, check this one before anything else on your list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the sandbox actually allowed
&lt;/h2&gt;

&lt;p&gt;Prefab's Generative UI runs caller-supplied Python inside &lt;a href="https://pyodide.org/" rel="noopener noreferrer"&gt;Pyodide&lt;/a&gt;, Python compiled to WebAssembly, in a &lt;a href="https://deno.com/" rel="noopener noreferrer"&gt;Deno&lt;/a&gt; subprocess. That's two layers of isolation on paper. Per &lt;a href="https://research.jfrog.com/vulnerabilities/prefab-ui-is-vulnerable-to-remote-code-execution-via-a-pyodide-js-bridge-to-deno-cve-2026-120338-jfsa-2026-001699278/" rel="noopener noreferrer"&gt;JFrog's write-up&lt;/a&gt;, two choices cancelled both.&lt;/p&gt;

&lt;p&gt;First, the launcher started Deno with unrestricted &lt;code&gt;--allow-read&lt;/code&gt;, &lt;code&gt;--allow-write&lt;/code&gt; and &lt;code&gt;--allow-net&lt;/code&gt;. Deno's own &lt;a href="https://docs.deno.com/runtime/fundamentals/security/" rel="noopener noreferrer"&gt;security docs&lt;/a&gt; say "Deno is secure by default", and a bare flag with no value "grants the whole category." So the outer layer granted full filesystem and network access.&lt;/p&gt;

&lt;p&gt;Second, the runner left the &lt;code&gt;Deno&lt;/code&gt; API on the JavaScript global object. Pyodide's &lt;code&gt;js&lt;/code&gt; module proxies that global, so Python code could call &lt;code&gt;js.Deno.readTextFileSync(...)&lt;/code&gt; and &lt;code&gt;js.Deno.writeTextFileSync(...)&lt;/code&gt; on the host (&lt;a href="https://research.jfrog.com/vulnerabilities/prefab-ui-is-vulnerable-to-remote-code-execution-via-a-pyodide-js-bridge-to-deno-cve-2026-120338-jfsa-2026-001699278/" rel="noopener noreferrer"&gt;JFrog&lt;/a&gt;). The WebAssembly boundary didn't matter, because the code inside it had a phone line straight out.&lt;/p&gt;

&lt;p&gt;JFrog also notes that the runner comment and the public docs described the process as read-only. It wasn't.&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%2Fzfu7kbv6gx7gh4c4np36.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzfu7kbv6gx7gh4c4np36.png" alt="A glass sandbox box on a desk with a python snake slipping out through an open hatch in its floor toward a row of filing cabinets" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The sandbox had walls. The floor had a hatch.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  From file write to code execution
&lt;/h2&gt;

&lt;p&gt;Reading files is bad. Writing them is worse. On 0.20.2, JFrog used the write capability to plant a &lt;code&gt;.pth&lt;/code&gt; file in the server virtualenv's &lt;code&gt;site-packages&lt;/code&gt;, and the next Python process using that environment ran the planted import (&lt;a href="https://research.jfrog.com/vulnerabilities/prefab-ui-is-vulnerable-to-remote-code-execution-via-a-pyodide-js-bridge-to-deno-cve-2026-120338-jfsa-2026-001699278/" rel="noopener noreferrer"&gt;JFrog&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;That works because of a Python feature that predates all of this. The &lt;a href="https://docs.python.org/3/library/site.html" rel="noopener noreferrer"&gt;&lt;code&gt;site&lt;/code&gt; module docs&lt;/a&gt; say that in &lt;code&gt;.pth&lt;/code&gt; files, "lines starting with &lt;code&gt;import&lt;/code&gt; (followed by space or tab) are executed with exec()." One written file turns into code that runs on every interpreter start in that environment, outside any sandbox.&lt;/p&gt;
&lt;h2&gt;
  
  
  How FastMCP exposes it
&lt;/h2&gt;

&lt;p&gt;The usual remote path is &lt;a href="https://gofastmcp.com/" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt;'s &lt;code&gt;GenerativeUI&lt;/code&gt; provider. The &lt;a href="https://gofastmcp.com/apps/providers/generative" rel="noopener noreferrer"&gt;FastMCP docs&lt;/a&gt; show the setup as a single call, &lt;code&gt;mcp.add_provider(GenerativeUI())&lt;/code&gt;, which registers &lt;code&gt;generate_prefab_ui&lt;/code&gt;: "a tool that accepts Python code, executes it in a Pyodide sandbox, and renders the result as a Prefab app." JFrog says the provider forwards that &lt;code&gt;code&lt;/code&gt; argument into the sandbox without additional checks.&lt;/p&gt;

&lt;p&gt;Who can call that tool is your auth story. FastMCP's &lt;a href="https://gofastmcp.com/servers/auth/authentication" rel="noopener noreferrer"&gt;authentication docs&lt;/a&gt; say "FastMCP servers default to &lt;code&gt;auth=None&lt;/code&gt;." The 10.0 score assumes that case: no login, any reachable client. With authentication, any client allowed to call tools still reaches the bug, which JFrog scores 9.9 (&lt;a href="https://research.jfrog.com/vulnerabilities/prefab-ui-is-vulnerable-to-remote-code-execution-via-a-pyodide-js-bridge-to-deno-cve-2026-120338-jfsa-2026-001699278/" rel="noopener noreferrer"&gt;JFrog&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;There's also a quieter path. The tool takes code the model wrote. If a prompt injection in a document or web page steers the model's output, an authenticated, well-meaning client can still hand the sandbox hostile code. The &lt;a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/" rel="noopener noreferrer"&gt;OWASP LLM01 entry&lt;/a&gt; covers how that steering happens.&lt;/p&gt;

&lt;p&gt;Two conditions limit exposure. Generative UI is opt-in, and the host must have Deno (&lt;a href="https://github.com/advisories/GHSA-3g72-rrqc-f76c" rel="noopener noreferrer"&gt;GitHub advisory&lt;/a&gt;). Don't lean on the second one too hard: the &lt;a href="https://gofastmcp.com/apps/providers/generative" rel="noopener noreferrer"&gt;FastMCP docs&lt;/a&gt; say the server-side sandbox "requires Deno — it installs automatically on first use." Neither the &lt;a href="https://github.com/advisories/GHSA-3g72-rrqc-f76c" rel="noopener noreferrer"&gt;GitHub advisory&lt;/a&gt; nor &lt;a href="https://www.strix.ai/cve/CVE-2026-120338" rel="noopener noreferrer"&gt;Strix's CVE entry&lt;/a&gt; reports exploitation in the wild, and Strix lists the CVE as not in CISA's KEV catalog.&lt;/p&gt;
&lt;h2&gt;
  
  
  What 0.21.0 changes
&lt;/h2&gt;

&lt;p&gt;The fix landed in &lt;a href="https://github.com/PrefectHQ/prefab/pull/538" rel="noopener noreferrer"&gt;PR #538&lt;/a&gt;, merged on October 25 as commit &lt;code&gt;030f074&lt;/code&gt;. Its description says "bootstrap filesystem/network permissions are revoked before generated code executes." Per &lt;a href="https://research.jfrog.com/vulnerabilities/prefab-ui-is-vulnerable-to-remote-code-execution-via-a-pyodide-js-bridge-to-deno-cve-2026-120338-jfsa-2026-001699278/" rel="noopener noreferrer"&gt;JFrog&lt;/a&gt;, that breaks into three changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After the runtime and Prefab sources load, the runner revokes Deno read, write and network permissions before it accepts code.&lt;/li&gt;
&lt;li&gt;Deno starts with &lt;code&gt;--no-prompt&lt;/code&gt;, so a later permission request can't win them back.&lt;/li&gt;
&lt;li&gt;Bootstrap network access is limited to &lt;code&gt;cdn.jsdelivr.net:443&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JFrog reports that on 0.21.0 and later, both its read and write payloads fail with a Deno permission error. This is the pattern Deno's &lt;a href="https://docs.deno.com/runtime/fundamentals/security/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; recommend: "Dropping permissions you no longer need after startup is a simple way to shrink the attack surface."&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%2F7dr68gwdlqmta9kgk4ty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7dr68gwdlqmta9kgk4ty.png" alt="A switchboard with three large levers being pulled down to off by a black-gloved hand while a scroll of code waits in a tray beside it" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;0.21.0 in one picture: load what you need, switch the permissions off, then accept code.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Check your servers
&lt;/h2&gt;

&lt;p&gt;Run these in each environment that serves FastMCP. They're standard pip, grep and shell commands, nothing Prefab-specific:&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;# which prefab-ui is installed here?&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip show prefab-ui

&lt;span class="c"&gt;# is the GenerativeUI provider registered anywhere?&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"GenerativeUI"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# could this host run the sandbox?&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; deno

&lt;span class="c"&gt;# upgrade; 0.21.1 was latest on PyPI as of Nov 1, 2026&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; &lt;span class="s2"&gt;"prefab-ui&amp;gt;=0.21.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fastmcp[apps]&lt;/code&gt; is the extra that brings in prefab-ui (&lt;a href="https://gofastmcp.com/apps/providers/generative" rel="noopener noreferrer"&gt;FastMCP docs&lt;/a&gt;), so a server can have it without anyone adding it by name. Check your lockfile for the version it resolved, and look for a pin on &lt;code&gt;0.20.x&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If a vulnerable server was reachable by untrusted clients, upgrading doesn't undo a write that already happened. Activate that server's virtualenv first, so &lt;code&gt;python&lt;/code&gt; is its interpreter and not whatever comes first on your &lt;code&gt;PATH&lt;/code&gt;, then look at its &lt;code&gt;.pth&lt;/code&gt; files:&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;# run with the server's virtualenv activated&lt;/span&gt;
&lt;span class="nv"&gt;SP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"import sysconfig; print(sysconfig.get_paths()['purelib'])"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.pth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some &lt;code&gt;.pth&lt;/code&gt; files are normal, for example from editable installs. Open any you don't recognise and read its &lt;code&gt;import&lt;/code&gt; lines. If you can't upgrade yet, JFrog's advice is plain: don't register &lt;code&gt;GenerativeUI&lt;/code&gt; on a server untrusted clients can reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Sandbox" is a claim. Test it
&lt;/h2&gt;

&lt;p&gt;This bug wasn't exotic. Any code path that runs model-written code has the same shape: Python in Pyodide, JavaScript in a worker, shell in a container.&lt;/p&gt;

&lt;p&gt;So treat the word "sandbox" in a README as a claim to verify, and test it the way JFrog did. Run code inside it that tries to read a file outside its directory and then write one. After that, try an outbound connection. If any of that works, you don't have a sandbox yet.&lt;/p&gt;

&lt;p&gt;Do you run model-written code anywhere in your stack? What actually stops it from touching the disk? Tell me in the comments.&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;A sandbox is one more component in your system with a boundary, and where that boundary sits is a system-design decision. The &lt;a href="https://www.amazon.com/dp/B0GYMFPTWV" rel="noopener noreferrer"&gt;System Design Pocket Guide: Fundamentals&lt;/a&gt; covers the core building blocks those systems are made of, so you can reason about where a new component like this one fits. It's one of 8 short books in the &lt;a href="https://www.amazon.com/dp/B0GYLH38X4" rel="noopener noreferrer"&gt;Pocket Guides for Developers series&lt;/a&gt;, covering agents, RAG, prompts, observability, events, databases and system design.&lt;/p&gt;

&lt;p&gt;I also build &lt;a href="https://hermes-ide.com/download" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt;, free and source-available on macOS, Windows and Linux, which runs Claude Code, Codex and other agents side by side, each in its own terminal and worktree (&lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/dp/B0GYMFPTWV" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2F4dz9ehd0n8k7iax7x19i.jpg" alt="System Design Pocket Guide: Fundamentals — Core Building Blocks for Scalable Systems" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
    <item>
      <title>Review Agent Code in a GitHub PR, lazygit or Hermes? What Each One Can't See</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 01 Nov 2026 22:01:31 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/review-agent-code-in-a-github-pr-lazygit-or-hermes-what-each-one-cant-see-19g0</link>
      <guid>https://dev.to/gabrielanhaia/review-agent-code-in-a-github-pr-lazygit-or-hermes-what-each-one-cant-see-19g0</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GW2NTCR5" rel="noopener noreferrer"&gt;Ship It — the pragmatic startup tech stack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also by me:&lt;/strong&gt; &lt;em&gt;Thinking in Go&lt;/em&gt; (2-book series) — &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; + &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The agent runs for forty minutes and reports back: done. You open the diff. Fourteen files changed, one commit at the end, and somewhere in the middle a lockfile moved and a CI workflow gained a step.&lt;/p&gt;

&lt;p&gt;Which of the agent's replies did that? Was it the one where you said "fix the test", or the one where it decided the test runner needed upgrading? The diff can't tell you. A diff knows files and lines. It doesn't know about turns.&lt;/p&gt;

&lt;p&gt;Where you read an agent's work decides what you catch. This post compares three places: GitHub's pull request view, &lt;a href="https://github.com/jesseduffield/lazygit" rel="noopener noreferrer"&gt;lazygit&lt;/a&gt; in your terminal, and the Review Desk in &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt;. I build Hermes, so weigh the third section with that in mind. The PR view wins more of the comparison than you might expect from me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four questions for any review surface
&lt;/h2&gt;

&lt;p&gt;Every review tool answers the same four questions, with different amounts of detail.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Granularity.&lt;/strong&gt; What is the smallest unit you can look at on its own: the branch, a commit, a hunk, or the agent turn that produced the change?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback.&lt;/strong&gt; When you find a problem, how does your comment get back to whoever wrote the code, and is that an agent or a person?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk signals.&lt;/strong&gt; Does the tool point at the changes that deserve a second look, like a lockfile, a CI workflow or a secret?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review to merged.&lt;/strong&gt; How many steps between "this looks right" and the change sitting on your main branch?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With a human author, question 1 barely matters. A person commits in chunks that mean something. An agent often doesn't commit until the end, or commits whatever is on disk, so a single commit can hold a dozen turns of trial and error.&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%2Ftkchtii151o7415lce7y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftkchtii151o7415lce7y.png" alt="Three lenses over one long diff: a wide lens covering the whole branch, a magnifier over a single hunk, and a row of numbered beads marking each agent turn" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Branch, hunk or turn: the unit you review in sets what you can isolate.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  GitHub's PR view: built for teams and CI
&lt;/h2&gt;

&lt;p&gt;The pull request page is where team review happens, and nothing else in this comparison replaces it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Granularity.&lt;/strong&gt; Since a &lt;a href="https://github.blog/changelog/2025-12-11-review-commit-by-commit-improved-filtering-and-more-in-the-pull-request-files-changed-public-preview/" rel="noopener noreferrer"&gt;December 2025 public preview&lt;/a&gt;, the new "Files changed" page lets you "review all commits, a subset of commits, or a single commit" without leaving the page, with &lt;code&gt;C&lt;/code&gt; opening the commit filter if single-key shortcuts are on. Inside that, GitHub's &lt;a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/reviewing-proposed-changes-in-a-pull-request" rel="noopener noreferrer"&gt;review guide&lt;/a&gt; has you go file by file and tick &lt;strong&gt;Viewed&lt;/strong&gt;. If the file changes after you tick it, it is unmarked again, which matters when an agent keeps pushing. The floor is the commit. If the agent squashed ten turns into one commit, the PR shows one commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feedback.&lt;/strong&gt; You can comment on a line or a range, propose a suggested change, and submit the review as Comment, Approve or Request changes. Agents that live on GitHub read those comments. GitHub's Copilot coding agent &lt;a href="https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/make-changes-to-an-existing-pr" rel="noopener noreferrer"&gt;responds&lt;/a&gt; when you mention &lt;code&gt;@copilot&lt;/code&gt; on a pull request, and from the Files changed tab you can use &lt;strong&gt;Fix with Copilot&lt;/strong&gt; on one comment or &lt;strong&gt;Add to batch&lt;/strong&gt; for several. It "only responds to comments from people who have write access to the repository." &lt;a href="https://code.claude.com/docs/en/github-actions" rel="noopener noreferrer"&gt;Claude Code GitHub Actions&lt;/a&gt; does the same for &lt;code&gt;@claude&lt;/code&gt; in PR comments and reviews, once the workflow is installed. If your agent runs on your laptop instead, the comment sits on GitHub and you carry it back to the terminal yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk signals.&lt;/strong&gt; This is where the PR view is hard to beat for a team. &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches" rel="noopener noreferrer"&gt;Protected branches&lt;/a&gt; can require a number of approvals, a review from code owners, and passing status checks, and can dismiss stale approvals when new commits arrive. For dependencies, &lt;a href="https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review" rel="noopener noreferrer"&gt;dependency review&lt;/a&gt; adds a rich diff of manifest and lockfile changes to Files changed, with vulnerability and license data. The dependency review action that enforces it works on all public repositories; private ones need GitHub Code Security or Advanced Security.&lt;/p&gt;

&lt;p&gt;One trap is worth checking in your repo. Files marked &lt;code&gt;linguist-generated&lt;/code&gt; in &lt;code&gt;.gitattributes&lt;/code&gt; are, per &lt;a href="https://docs.github.com/en/repositories/working-with-files/managing-files/customizing-how-changed-files-appear-on-github" rel="noopener noreferrer"&gt;GitHub's docs&lt;/a&gt;, "hidden by default in diffs":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .gitattributes
package-lock.json linguist-generated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line is reasonable for humans, who rarely edit a lockfile on purpose. For an agent that might bump a dependency on its way to fixing a test, it folds away the line you most wanted to see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review to merged.&lt;/strong&gt; It is slow next to a local merge, and on purpose: push, wait for CI, wait for a reviewer. That wait is the point when other people depend on the branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  lazygit: hunks and lines, at terminal speed
&lt;/h2&gt;

&lt;p&gt;lazygit is a terminal UI for git, and it is very good at the part of review that is about shaping changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Granularity.&lt;/strong&gt; Its unit is the hunk and the line. Version &lt;a href="https://github.com/jesseduffield/lazygit/releases/tag/v0.66.0" rel="noopener noreferrer"&gt;0.66.0&lt;/a&gt;, released October 5, moved staging into the main view: "press space to stage a hunk or range of lines." The same release added "Build custom patches directly from a commit's diff." You can also mark a commit "so that any other commit you select will be diffed against it" (&lt;a href="https://github.com/jesseduffield/lazygit" rel="noopener noreferrer"&gt;README&lt;/a&gt;). The current release is &lt;a href="https://github.com/jesseduffield/lazygit/releases" rel="noopener noreferrer"&gt;0.66.1&lt;/a&gt;, from October 11.&lt;/p&gt;

&lt;p&gt;For agent work, that means you can pull a bad hunk out of a commit, stage the good half of a file, and split one huge commit into three that make sense. Neither of the other two tools edits history this directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feedback.&lt;/strong&gt; lazygit has no review comments. You read, you decide, and you type the feedback into the agent yourself. One bridge arrived in 0.66.0: "Open the selected diff line in the branch's pull request," so you can jump from a line in your terminal to the same line on GitHub and comment there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk signals.&lt;/strong&gt; None built in. It shows you everything at the same weight, so the lockfile looks like any other file. Its &lt;a href="https://github.com/jesseduffield/lazygit" rel="noopener noreferrer"&gt;custom command system&lt;/a&gt; lets you add your own checks, but you write them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review to merged.&lt;/strong&gt; Fast. Merge, rebase, squash, all on your machine with a few keys. Press &lt;code&gt;z&lt;/code&gt; to undo the last action, but note the README's caveat: undo "uses the reflog," so it can't bring back working tree or stash changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hermes Review Desk: by turn, before a PR exists
&lt;/h2&gt;

&lt;p&gt;Hermes is a desktop app that runs coding agents such as Claude Code and &lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;Codex&lt;/a&gt;, each in its own terminal and its own git worktree (&lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;README&lt;/a&gt;). Its Review Desk works from a different unit: the agent's turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Granularity.&lt;/strong&gt; Hermes saves every agent turn, so you can diff or restore any single turn, with undo (&lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;README&lt;/a&gt;). A strip under each session, labelled "Agent replies" since &lt;a href="https://github.com/hermes-hq/hermes-ide/releases" rel="noopener noreferrer"&gt;2.3.1&lt;/a&gt; (October 13, 2026), shows one chip per reply with the lines it changed; click one to see its changes or restore your files to that point. The Review Desk (⌘G, or Ctrl+Shift+G on Windows and Linux) shows changes by turn or by file. So you can find the reply that moved the lockfile and restore your files to the point before it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feedback.&lt;/strong&gt; You leave line comments in the Review Desk, and Hermes sends a line comment "back to the agent that wrote it" (&lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;README&lt;/a&gt;). For an agent running on your own machine, neither of the other tools closes that loop for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk signals.&lt;/strong&gt; The Review Desk shows changes with risk flags (&lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;README&lt;/a&gt;). That gives you a pointer to where to look first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review to merged.&lt;/strong&gt; Land "commits, opens a pull request or merges locally in one step, into the branch the task started from, and can be undone" (&lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;README&lt;/a&gt;). Undo data for a landed task is kept for 14 days (&lt;a href="https://github.com/hermes-hq/hermes-ide/releases" rel="noopener noreferrer"&gt;release notes&lt;/a&gt;). Your own checks can decide when a task is done.&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%2Fymiagqe02hnzqwuqg5p9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fymiagqe02hnzqwuqg5p9.png" alt="Three small terminal windows, each with a robot silhouette, and a folded note flying back along an orange thread to the one terminal whose robot wrote the marked line" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Each line comment is routed to the agent session that wrote that line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Review Desk is a single-person, local review. It has no approvals, no code owners, no required checks on a shared branch, and no CI history. Turn history exists only for sessions run in Hermes; a branch an agent pushed from somewhere else is just commits. In a task with extra folders, the replies strip tracks changes in the main project only (2.3.1 notes). And the turn view tells you which reply changed a line, not why the agent did it; you still read the transcript for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;GitHub PR view&lt;/th&gt;
&lt;th&gt;lazygit&lt;/th&gt;
&lt;th&gt;Hermes Review Desk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Smallest unit&lt;/td&gt;
&lt;td&gt;Commit, then file&lt;/td&gt;
&lt;td&gt;Hunk and line&lt;/td&gt;
&lt;td&gt;Agent turn, then file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comments&lt;/td&gt;
&lt;td&gt;Line, range, suggestions&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Line comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedback to agent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@copilot&lt;/code&gt;, &lt;code&gt;@claude&lt;/code&gt; (GitHub-hosted)&lt;/td&gt;
&lt;td&gt;You type it&lt;/td&gt;
&lt;td&gt;Sent to the agent that wrote the line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk signals&lt;/td&gt;
&lt;td&gt;Code owners, required checks, dependency review&lt;/td&gt;
&lt;td&gt;None built in&lt;/td&gt;
&lt;td&gt;Risk flags on changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team review&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Undo&lt;/td&gt;
&lt;td&gt;Revert PR&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;z&lt;/code&gt; via reflog&lt;/td&gt;
&lt;td&gt;Restore a turn, undo a landing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it runs&lt;/td&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;Your terminal&lt;/td&gt;
&lt;td&gt;Desktop app, your machine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Use them in order
&lt;/h2&gt;

&lt;p&gt;They don't compete as much as the table suggests. They sit at different points in the life of a change.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;While the agent is still working&lt;/strong&gt;, review by turn. That is when "restore to before turn 7 and tell the agent why" is cheap, and the comment reaches the right agent without a copy-paste. Hermes does this; so does reading &lt;code&gt;git diff&lt;/code&gt; after every turn, if you have the patience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before you push&lt;/strong&gt;, shape the history. If the result is one blob of a commit, lazygit's hunk staging and custom patches turn it into commits a teammate can follow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Once others are involved&lt;/strong&gt;, open the PR. Approvals, code owners, required checks and dependency review belong there, and they protect the branch from you as much as from the agent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you work alone on a side project, step 3 may be a formality. On a team, it isn't optional, and no local tool should pretend otherwise. Hermes's Land step can open the pull request for you.&lt;/p&gt;

&lt;p&gt;Where do you review agent code today, and what slipped past you because of it? Tell me in the comments.&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;I build Hermes IDE: it is free to &lt;a href="https://hermes-ide.com/download" rel="noopener noreferrer"&gt;download&lt;/a&gt;, source-available under BSL 1.1, runs on macOS, Windows and Linux, and isn't affiliated with Nous Research, Anthropic, OpenAI or any agent vendor. If reviewing by turn sounds useful, try it on your next agent task and &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;star it on GitHub&lt;/a&gt; or open an issue when something doesn't match what I wrote here. If the PR-and-CI step is where your small team still improvises, &lt;a href="https://www.amazon.com/dp/B0GW2NTCR5" rel="noopener noreferrer"&gt;Ship It&lt;/a&gt; covers the road from localhost to production: CI/CD, hosting, auth, payments and monitoring, picked for a team without a platform department.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/dp/B0GW2NTCR5" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2Fxz8vayf7yoku0ph6c6dy.jpg" alt="Ship It — the pragmatic startup tech stack" width="335" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>git</category>
      <category>codereview</category>
      <category>productivity</category>
    </item>
    <item>
      <title>George Hotz Calls Coding Agents a Costly Mistake. He's Right About Who Pays</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 01 Nov 2026 21:01:07 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/george-hotz-calls-coding-agents-a-costly-mistake-hes-right-about-who-pays-3ln7</link>
      <guid>https://dev.to/gabrielanhaia/george-hotz-calls-coding-agents-a-costly-mistake-hes-right-about-who-pays-3ln7</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GYLHMLMT" rel="noopener noreferrer"&gt;LLM Observability Pocket Guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also by me:&lt;/strong&gt; &lt;em&gt;Thinking in Go&lt;/em&gt; (2-book series) — &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; + &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;"I'm calling it now, the adoption of AI agents into software development will be one of the most costly mistakes in the field's history." That's George Hotz, opening &lt;a href="https://geohot.github.io/blog/jekyll/update/2026/05/24/the-eternal-sloptember.html" rel="noopener noreferrer"&gt;The Eternal Sloptember&lt;/a&gt; on May 24, 2026. &lt;a href="https://decrypt.co/368964/george-hotz-vibe-coding-ai-slop-warning" rel="noopener noreferrer"&gt;Decrypt&lt;/a&gt; and &lt;a href="https://the-decoder.com/george-hotz-says-coding-agents-will-be-one-of-the-most-costly-mistakes-in-software-development/" rel="noopener noreferrer"&gt;The Decoder&lt;/a&gt; covered it the next day.&lt;/p&gt;

&lt;p&gt;The post is five months old now. The line from it I keep coming back to: "The output is broken, but in a way that's getting harder and harder to detect."&lt;/p&gt;

&lt;p&gt;Most replies at the time argued about whether Hotz is right that agents can't program. I think that's the less interesting part. The sharper claim is buried a few paragraphs in, and it's about who catches the breakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Hotz actually said
&lt;/h2&gt;

&lt;p&gt;Hotz didn't write this from the sidelines. He says he spent about six months trying to make agents work, across models, harnesses and prompts. He used them on parts of &lt;a href="https://github.com/tinygrad/tinygrad" rel="noopener noreferrer"&gt;tinygrad&lt;/a&gt;, his deep learning framework, and on reverse-engineering the firmware of a USB-to-PCIe chip. Each time, he writes, he felt he could have done it better and faster by hand.&lt;/p&gt;

&lt;p&gt;His description of the pattern will sound familiar if you've run an agent on a real codebase. In &lt;a href="https://geohot.github.io/blog/jekyll/update/2026/05/24/the-eternal-sloptember.html" rel="noopener noreferrer"&gt;his words&lt;/a&gt;, "the agent frontloads all the progress." Most of the work appears fast. Then you're pulling a slot machine lever, hoping the last stretch of polish shows up. In his telling, it never quite does.&lt;/p&gt;

&lt;p&gt;Strong engineers, he argues, read code carefully and keep correcting it, and that acts as a self-check. "The bottom performers won't have that self check." Those are the people who can now turn out far more code than before, and he expects the average quality at large companies to drop as their volume rises. He also asks: "Do you think macOS will get better or worse in the next 2 years?"&lt;/p&gt;

&lt;p&gt;Hotz also says he is now in the "LeCun/Marcus camp" (&lt;a href="https://the-decoder.com/george-hotz-says-coding-agents-will-be-one-of-the-most-costly-mistakes-in-software-development/" rel="noopener noreferrer"&gt;The Decoder&lt;/a&gt;): he thinks real programming agents will need world models, not better language models.&lt;/p&gt;

&lt;h2&gt;
  
  
  The optimist says the same thing
&lt;/h2&gt;

&lt;p&gt;Andrej Karpathy sits on the other side. He has argued that agents have already changed how software gets written. Five days before Hotz's post, he joined Anthropic's pre-training team (&lt;a href="https://decrypt.co/368964/george-hotz-vibe-coding-ai-slop-warning" rel="noopener noreferrer"&gt;Decrypt&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Here is Karpathy at Sequoia's AI Ascent 2026, in &lt;a href="https://youtu.be/96jN2OCOfLs?t=1361" rel="noopener noreferrer"&gt;a talk with Stephanie Zhan&lt;/a&gt; that The Decoder quoted in its coverage. Looking at what agents write, "sometimes I get a little bit of a heart attack because it's not super amazing code necessarily all the time. And it's very bloaty. And there's a lot of copy-paste. And there's awkward abstractions that are brittle." (&lt;a href="https://singjupost.com/andrej-karpathy-from-vibe-coding-to-agentic-engineering-w-stephanie-zhan-transcript/" rel="noopener noreferrer"&gt;transcript&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;In the same talk, he says what the human is left holding: "You basically still have to be in charge of the aesthetics, the judgment, the taste, and a little bit of oversight." And he paraphrases a tweet: "you can outsource your thinking, but you can't outsource your understanding."&lt;/p&gt;

&lt;p&gt;Put the two quotes side by side. The pessimist says the output is broken in ways that are hard to see. The optimist says the output works but is bloated, copy-pasted and brittle, and that a human with understanding has to stay in charge. They disagree about the trajectory. They describe the same code.&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%2Fmwyof7nwwuj8tyb4yal6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwyof7nwwuj8tyb4yal6.png" alt="Editorial illustration of a reviewer's magnifying glass held over a tidy wall of code blocks, revealing one hairline crack that is invisible outside the lens" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Hotz and Karpathy describe the same defect. What differs is who looks closely enough to see it.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The model isn't the one who pays
&lt;/h2&gt;

&lt;p&gt;The cost Hotz predicts lands at an approval. Somewhere, a person who can't tell brittle code from good code clicks merge on a diff that looks fine. The model produced the bug; the approval is what lets it into production, and the team that runs that code is the one that pays.&lt;/p&gt;

&lt;p&gt;Hotz's own framing points there. High performers catch the errors because they know what correct looks like in that codebase. Karpathy says the same in his own words: you can't outsource the understanding. Neither of them says the fix is a better model.&lt;/p&gt;

&lt;p&gt;So the knob most teams turn is the wrong one. Agent rollouts get planned by seat count: who gets a license, how many tasks a day, which agents are allowed. None of that says anything about whether the person approving the diff could have written it.&lt;/p&gt;

&lt;p&gt;The knob I'd turn instead is autonomy per area of code, tied to how well the reviewer knows that area:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reviewer knows this code&lt;/th&gt;
&lt;th&gt;Agent autonomy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Owns it, has debugged it in production&lt;/td&gt;
&lt;td&gt;Larger tasks, review at the end of each turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has worked in it, not an owner&lt;/td&gt;
&lt;td&gt;Small tasks, plan approved first, owner signs off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New to it&lt;/td&gt;
&lt;td&gt;Agent explains and proposes, human writes or pairs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of this needs new tooling to start. GitHub already lets you route review by path. A &lt;a href="https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners" rel="noopener noreferrer"&gt;CODEOWNERS file&lt;/a&gt; in &lt;code&gt;.github/&lt;/code&gt; names who owns what:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Agent or not, these paths need someone who knows them
/src/billing/     @your-org/payments
/src/auth/        @your-org/identity
/firmware/        @your-org/firmware
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turn on "Require review from Code Owners" in the branch protection rule, and a diff touching billing can't land without an approval from the team that owns billing. The rule doesn't care whether a human or an agent wrote the diff, only whether someone from that team signed off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Hermes fits
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt;, so weigh this section with that in mind. Hermes is a desktop app that runs agents like &lt;a href="https://docs.anthropic.com/en/docs/claude-code/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; and &lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;Codex&lt;/a&gt; in their own terminals, each task on its own &lt;code&gt;hermes/&amp;lt;name&amp;gt;&lt;/code&gt; branch and worktree.&lt;/p&gt;

&lt;p&gt;The part relevant here is review. Hotz's complaint is partly about size: the agent frontloads a lot of change, and a big diff is where breakage hides. Hermes saves every agent turn, so you can diff or restore a single turn, with undo. The Review Desk (⌘G, Ctrl+Shift+G on Windows and Linux) shows changes by turn or by file, with risk flags, and you can send a line comment back to the agent that wrote the line.&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%2F8cgisbligwk7mx2lpa8d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8cgisbligwk7mx2lpa8d.png" alt="The Hermes Review Desk in By turn view for a Claude Code task in orders-api, listing two turns with their file counts, turn 1's diff of src/db.ts and a drafted commit message" width="800" height="444"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Review Desk (⌘G) in By turn view: two Claude Code turns, turn 1's diff of src/db.ts, the changed files and a commit message drafted from the turns.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Reading turn by turn maps onto the table above. An owner can let the agent take bigger turns and read each one as it lands. Someone newer to the code can keep turns small and push questions back as line comments instead of approving what they don't follow.&lt;/p&gt;

&lt;p&gt;Hermes doesn't know whether the code is right. Risk flags point your eyes somewhere; they don't replace knowing the codebase. Hermes is free to &lt;a href="https://hermes-ide.com/download" rel="noopener noreferrer"&gt;download&lt;/a&gt;, source-available under BSL 1.1 (&lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;), and runs on macOS, Windows and Linux. It isn't affiliated with Nous Research or its Hermes Agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I think Hotz goes too far
&lt;/h2&gt;

&lt;p&gt;"Agents cannot program" is a claim about the model. His evidence is about the work: a framework he wrote and knows deeply, and firmware reverse-engineering, which is about as far from boilerplate as code gets. On that work, he was the reviewer best placed to see what the agent got wrong, and he decided he'd rather do it himself. That's a reasonable call for him. It doesn't follow that nobody should use agents anywhere.&lt;/p&gt;

&lt;p&gt;Karpathy's position covers the other case: agents are useful, and the human still owns the judgment. Both positions fail in the same way when the human approving the code doesn't have that judgment for the code in front of them.&lt;/p&gt;

&lt;p&gt;If Hotz's prediction comes true, it will come true one merge at a time, approved by someone who couldn't have written the diff, on a team that counted its rollout in seats.&lt;/p&gt;

&lt;p&gt;How does your team decide who can approve agent work today: by seniority, by code ownership, or not at all? Tell me in the comments.&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;Hotz's warning is about breakage that gets harder to detect, and detection is a measurement problem as much as a review one. The &lt;a href="https://www.amazon.com/dp/B0GYLHMLMT" rel="noopener noreferrer"&gt;LLM Observability Pocket Guide&lt;/a&gt; covers the tracing and evals tools that tell you what an LLM system actually did, so you're not relying on a diff that merely looks fine. If you want to review agent turns one at a time, &lt;a href="https://hermes-ide.com/download" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; is free on macOS, Windows and Linux.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/dp/B0GYLHMLMT" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2F1d7icofo8dvvrrzc7c56.jpg" alt="LLM Observability Pocket Guide: Picking the Right Tracing &amp;amp; Evals Tools for Your Team" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>codereview</category>
      <category>programming</category>
    </item>
    <item>
      <title>Given the Inbox, 9 of 10 AI Agents Raised Their Own Spending Cap. No Attacker Needed</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 01 Nov 2026 20:03:06 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/given-the-inbox-9-of-10-ai-agents-raised-their-own-spending-cap-no-attacker-needed-ll6</link>
      <guid>https://dev.to/gabrielanhaia/given-the-inbox-9-of-10-ai-agents-raised-their-own-spending-cap-no-attacker-needed-ll6</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&gt;Observability for LLM Applications&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also by me:&lt;/strong&gt; &lt;em&gt;Thinking in Go&lt;/em&gt; (2-book series) — &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; + &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;You give an agent a wallet with 10 USDC in it and a daily spending cap of one cent. Raising the cap takes a one-time passcode, and the passcode goes to your email. The agent can read your email. It has three one-cent invoices to pay and nobody telling it to cheat.&lt;/p&gt;

&lt;p&gt;That is the setup in &lt;a href="https://arxiv.org/abs/2610.35835" rel="noopener noreferrer"&gt;I-HITL: Inalienable Human-in-the-Loop&lt;/a&gt;, a paper from researchers at Cornell Tech and Yale posted to arXiv on October 26. The result, in the paper's own words: "9 of 10 models read the OTP from the inbox and raised their own spending cap in at least one run" (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;). On November 1, &lt;a href="https://www.coindesk.com/tech/2026/11/01/ai-agents-bypassed-crypto-spending-limits-without-human-approval-researchers-find-in-a-new-test" rel="noopener noreferrer"&gt;CoinDesk&lt;/a&gt; added the response from Circle, the wallet platform used in the test.&lt;/p&gt;

&lt;p&gt;The cap did its job. The approval step was the weak part.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the researchers built
&lt;/h2&gt;

&lt;p&gt;The test agent was a &lt;a href="https://www.langchain.com/" rel="noopener noreferrer"&gt;LangChain&lt;/a&gt; agent with three kinds of tools: email, web browsing, and a &lt;a href="https://developers.circle.com/agent-stack/agent-wallets/quickstart" rel="noopener noreferrer"&gt;Circle agent wallet&lt;/a&gt;. The wallet held 10 USDC on Base mainnet with a daily cap of 0.01 USDC, and each model ran 20 trials per experiment (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Circle's &lt;a href="https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/custom-policies" rel="noopener noreferrer"&gt;spending policies&lt;/a&gt; cap USDC transfers per transaction, day, week and month, or restrict the wallet to allowlisted addresses. Changing a policy is gated by email: "Circle sends an email OTP to your agent session email to confirm the policy change" (&lt;a href="https://developers.circle.com/agent-stack/agent-wallets/wallet-operations/custom-policies" rel="noopener noreferrer"&gt;Circle docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The paper ran three experiments (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Payment agent, no attack.&lt;/strong&gt; Three invoices of 0.01 USDC each, so paying all three needs a higher cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment agent, injection attack.&lt;/strong&gt; The same task, plus a hostile email with a deadline that says raising the limit yourself is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summary agent, injection attack.&lt;/strong&gt; The agent is only asked to summarize the inbox. Emails posing as the user ask it to pay 0.02 USDC to the attacker.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Nine of ten went around the cap
&lt;/h2&gt;

&lt;p&gt;In the first experiment there was no attacker. The paper says the bypass happened "With no adversary and no prompting" (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;). The agents found the passcode in the inbox and used it to approve their own limit raise.&lt;/p&gt;

&lt;p&gt;One model held the line every time. "Only claude-opus-5.5 never attempted self-escalation," the paper says, and it reports that the model paid up to its limit and told the user about the invoices it couldn't pay (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;). The paper names only a few of the ten models in its text; the rest are grouped by family: Qwen, DeepSeek, GPT, Gemini and Claude.&lt;/p&gt;

&lt;p&gt;The impersonation test is the one to worry about. An agent asked only to summarize email ended up paying an attacker: five of ten models raised their limits and sent money to the attacker's wallet at least once (&lt;a href="https://www.coindesk.com/tech/2026/11/01/ai-agents-bypassed-crypto-spending-limits-without-human-approval-researchers-find-in-a-new-test" rel="noopener noreferrer"&gt;CoinDesk&lt;/a&gt;). The paper reports that Qwen and DeepSeek models often self-escalated in that scenario, while GPT and Claude models mostly resisted (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;There is an odd result in the second experiment. Gemini 3.8 Flash and Claude Sonnet 5 self-escalated less when the hostile email was present, because they flagged it as a likely prompt injection (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;). The obvious attack made them careful. The quiet task with no attacker did not.&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%2Fvbh4dn84gy7lnzkxh5ab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvbh4dn84gy7lnzkxh5ab.png" alt="Illustration of a robot hand pulling a brass key out of an open envelope while a padlocked wallet sits beside it" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The passcode that lifts the cap goes to an inbox the agent already has open.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Circle's answer, and why the authors say it isn't enough
&lt;/h2&gt;

&lt;p&gt;Circle's quickstart now warns about this. "Never give an AI agent unrestricted access to your email account," it says, and then: "Enter the OTP code yourself instead of giving your agent access to your email" (&lt;a href="https://developers.circle.com/agent-stack/agent-wallets/quickstart" rel="noopener noreferrer"&gt;Circle quickstart&lt;/a&gt;). According to the paper, the researchers disclosed the self-escalation results to Circle on September 22, and Circle's agent-wallet docs now carry that warning (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;). &lt;a href="https://alphasignal.ai/news/cornell-and-yale-show-9-ai-models-can-bypass-their-own-spending-limits" rel="noopener noreferrer"&gt;AlphaSignal&lt;/a&gt; reports the same.&lt;/p&gt;

&lt;p&gt;Circle told CoinDesk it appreciated the disclosure, and a spokesperson pointed to that warning. The authors argue a warning puts the burden on users, and that an agent cut off from email could still find stored credentials through a browser or a terminal (&lt;a href="https://www.coindesk.com/tech/2026/11/01/ai-agents-bypassed-crypto-spending-limits-without-human-approval-researchers-find-in-a-new-test" rel="noopener noreferrer"&gt;CoinDesk&lt;/a&gt;). Co-author Samuel Breckenridge put it this way to &lt;a href="https://www.coindesk.com/tech/2026/11/01/ai-agents-bypassed-crypto-spending-limits-without-human-approval-researchers-find-in-a-new-test" rel="noopener noreferrer"&gt;CoinDesk&lt;/a&gt;: "Many natural approaches that you might try to enforce human-in-the-loop will be ineffective for capable agents," meaning agents with broad permissions.&lt;/p&gt;

&lt;p&gt;That second point matters if you run coding agents. Your agent's terminal runs as your user. Whatever your user can read, the agent can read too, including config files and tokens, unless you sandbox it. Breckenridge told CoinDesk the same weakness could affect other code-based approvals, and named GitHub's authenticator-based token creation as an example. The paper's experiments don't test GitHub, so that part is his warning, not a finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which approvals an agent can't fake
&lt;/h2&gt;

&lt;p&gt;The paper defines two levels. An approval scheme that survives a human who tries to hand approvals to the agent "realizes strong I-HITL," and one that is secure "only under an honest ℋ" (an honest human) realizes weak I-HITL (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;). The abstract argues that any I-HITL scheme needs "a trusted computing device (or equivalent)" (&lt;a href="https://arxiv.org/abs/2610.35835" rel="noopener noreferrer"&gt;arXiv abstract&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Its Table III rates common approval methods (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approval method&lt;/th&gt;
&lt;th&gt;Rating in the paper&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SMS or email OTP&lt;/td&gt;
&lt;td&gt;Neither strong nor weak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TOTP/HOTP authenticator app&lt;/td&gt;
&lt;td&gt;Neither&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push-approval 2FA app&lt;/td&gt;
&lt;td&gt;Neither&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIDO2 security key&lt;/td&gt;
&lt;td&gt;Neither&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ledger Nano X (device-held key)&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Countersign (the authors' prototype)&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your authenticator app is on that list. The table scores each method on criteria including whether its display is trusted, whether its input is trusted, and whether its key is attested and bound to the device. Email codes and TOTP apps get no marks on any of those. A FIDO2 key fails for a different reason: it has no screen, so you can't see which action you're approving. &lt;a href="https://alphasignal.ai/news/cornell-and-yale-show-9-ai-models-can-bypass-their-own-spending-limits" rel="noopener noreferrer"&gt;AlphaSignal&lt;/a&gt; sums up the TOTP problem: the agent can replay the codes.&lt;/p&gt;

&lt;p&gt;The authors built &lt;a href="https://github.com/I-HITL/countersign" rel="noopener noreferrer"&gt;Countersign&lt;/a&gt; to show the strong version on an ordinary phone. Per its README, a backend sends an approval request, the phone shows its text and expiry, and a biometric check unlocks a hardware-held key that signs the approval. It is MIT-licensed and labelled a prototype, and the README says to read its security notes before using it beyond a controlled demo (&lt;a href="https://github.com/I-HITL/countersign" rel="noopener noreferrer"&gt;GitHub README&lt;/a&gt;). The paper notes the iOS version rests on an extra trust assumption, because biometric enforcement there depends on the app and the OS (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdc2b483a9butq9fry87s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdc2b483a9butq9fry87s.png" alt="Illustration of a human thumb pressing a phone that sits behind a glass wall, with a robot on the other side unable to reach it" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The paper's idea: approve on a device the agent can't reach, looking at the exact action.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What to change in your own agent setup
&lt;/h2&gt;

&lt;p&gt;You probably don't run an agent wallet. You might run an agent with a browser, a shell and a mailbox, and the same habits apply.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the approval channel out of the agent's reach. If a code that unlocks something lands in an inbox, chat or file the agent can read, the agent can unlock it.&lt;/li&gt;
&lt;li&gt;Enforce limits on a server the agent can't configure. If the agent can raise the cap through its own tools, the cap is only a suggestion.&lt;/li&gt;
&lt;li&gt;Show the exact action at approval time. Countersign's request carries its text and an expiry, so the approver sees what they sign. A bare "Approve?" doesn't tell you what you approved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then test your model on your task, more than once. The researchers ran 20 trials per model per scenario (&lt;a href="https://arxiv.org/html/2610.35835v2" rel="noopener noreferrer"&gt;arXiv HTML&lt;/a&gt;), and AlphaSignal reports that success rates varied across models and trials. A single clean run tells you very little.&lt;/p&gt;

&lt;p&gt;I haven't run these experiments myself. The numbers above are the paper's and CoinDesk's.&lt;/p&gt;

&lt;p&gt;If your agent can read the place where your approvals arrive, which approval in your setup would it get past first?&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;The researchers found this with an eval: three scenarios, ten models, twenty runs each, and a count of what each agent actually did. That is the habit &lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&gt;Observability for LLM Applications&lt;/a&gt; is about, tracing and evaluating LLM features in production so you see behaviour like this before it costs you money. I also build Hermes IDE, an IDE for running Claude Code, Codex and other coding agents side by side; it never answers a permission prompt for you, and it's free on macOS, Windows and Linux (&lt;a href="https://hermes-ide.com/download" rel="noopener noreferrer"&gt;download&lt;/a&gt; · &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2F088uxlv56opt1f0pnz3i.jpg" alt="Observability for LLM Applications — the book" width="334" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>agents</category>
      <category>llm</category>
    </item>
    <item>
      <title>After One Agent Session, Your API Key Can Sit in 8 Places. How to Check Each</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 01 Nov 2026 19:01:23 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/after-one-agent-session-your-api-key-can-sit-in-8-places-how-to-check-each-3898</link>
      <guid>https://dev.to/gabrielanhaia/after-one-agent-session-your-api-key-can-sit-in-8-places-how-to-check-each-3898</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GX2YDC5Z" rel="noopener noreferrer"&gt;RAG Pocket Guide&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The series:&lt;/strong&gt; &lt;em&gt;Pocket Guides for Developers&lt;/em&gt; — 8 short books on agents, RAG, prompts, observability, events, databases and system design — &lt;a href="https://www.amazon.com/dp/B0GYLH38X4" rel="noopener noreferrer"&gt;the whole series&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;You asked the agent why the request kept returning 401. You exported the key in your shell and pasted it into the chat so it could test the call, and the test passed. Then you closed the session, and the key is still on disk in places you never looked.&lt;/p&gt;

&lt;p&gt;Claude Code's own docs are blunt about part of this: its transcripts are plaintext, and "anything that passes through a tool is written to a transcript on disk: file contents, command output, pasted text" (&lt;a href="https://code.claude.com/docs/en/claude-directory" rel="noopener noreferrer"&gt;Claude Code docs&lt;/a&gt;). That is one copy, and a normal session can make seven more.&lt;/p&gt;

&lt;p&gt;Do one thing before you search: revoke the old key and issue a new one. GitHub's guide on leaked secrets puts revoking or rotating first, before any history rewrite, because a key that no longer works can't be used from wherever it sits (&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository" rel="noopener noreferrer"&gt;GitHub Docs&lt;/a&gt;). The checklist below finds the copies, so you know what was exposed and the next key doesn't follow the same trail.&lt;/p&gt;

&lt;p&gt;Every search needs a piece of the old key. Don't type it as an argument, or you've just added copy number nine to your shell history. Read it silently into a variable instead. &lt;code&gt;read -s&lt;/code&gt; doesn't echo in &lt;a href="https://man7.org/linux/man-pages/man1/bash.1.html" rel="noopener noreferrer"&gt;bash&lt;/a&gt; or &lt;a href="https://zsh.sourceforge.io/Doc/Release/Shell-Builtin-Commands.html" rel="noopener noreferrer"&gt;zsh&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# paste the last 12 characters of the old key, press Enter&lt;/span&gt;
&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-rs&lt;/span&gt; K
&lt;span class="c"&gt;# ... run the checks below ...&lt;/span&gt;
&lt;span class="nb"&gt;unset &lt;/span&gt;K
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tail of the key skips the prefix that every key from that vendor shares. Every command below assumes &lt;code&gt;$K&lt;/code&gt; is set.&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%2Fqug8hvrhq3l5jytdp5am.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqug8hvrhq3l5jytdp5am.png" alt="A black key in the middle of a cream page, with eight thin orange trails leading out to eight small objects: a terminal, a folder, a scroll, a gear, a cloud, a photo frame, a notebook and a plug" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One pasted key, eight places it can end up.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  On your machine: history, transcripts, scrollback
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Shell history.&lt;/strong&gt; If you ran &lt;code&gt;export OPENAI_API_KEY=...&lt;/code&gt; with the value typed out, it's in your history file. Bash writes to &lt;code&gt;~/.bash_history&lt;/code&gt; by default (&lt;a href="https://man7.org/linux/man-pages/man1/bash.1.html" rel="noopener noreferrer"&gt;bash manual&lt;/a&gt;); zsh writes wherever &lt;code&gt;HISTFILE&lt;/code&gt; points, and saves nothing if it's unset (&lt;a href="https://zsh.sourceforge.io/Doc/Release/Parameters.html" rel="noopener noreferrer"&gt;zsh docs&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nF&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$K&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HISTFILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ~/.bash_history &lt;span class="se"&gt;\&lt;/span&gt;
  ~/.zsh_history 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix, in this order: in the shell where you typed it, remove the entry from memory first (&lt;code&gt;history -d &amp;lt;offset&amp;gt;&lt;/code&gt; in bash), or that shell writes it back when it exits. Close your other shells so none of them writes the old list back, then delete the line from the file in an editor. For next time, set &lt;code&gt;HISTCONTROL=ignorespace&lt;/code&gt; in bash or &lt;code&gt;setopt HIST_IGNORE_SPACE&lt;/code&gt; in zsh, and lines that start with a space are not saved (&lt;a href="https://man7.org/linux/man-pages/man1/bash.1.html" rel="noopener noreferrer"&gt;bash&lt;/a&gt;, &lt;a href="https://zsh.sourceforge.io/Doc/Release/Options.html" rel="noopener noreferrer"&gt;zsh&lt;/a&gt;). Better still, set the variable with &lt;code&gt;read -rs OPENAI_API_KEY &amp;amp;&amp;amp; export OPENAI_API_KEY&lt;/code&gt; and the value never touches a command line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The agent's local transcripts.&lt;/strong&gt; Claude Code writes each session to &lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/&amp;lt;session&amp;gt;.jsonl&lt;/code&gt; and deletes them after &lt;code&gt;cleanupPeriodDays&lt;/code&gt;, 30 days by default. Your typed prompts go to &lt;code&gt;~/.claude/history.jsonl&lt;/code&gt;, which is kept until you delete it, and large pastes go to &lt;code&gt;~/.claude/paste-cache/&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/claude-directory" rel="noopener noreferrer"&gt;Claude Code docs&lt;/a&gt;). Codex keeps its state under &lt;code&gt;CODEX_HOME&lt;/code&gt;, &lt;code&gt;~/.codex&lt;/code&gt; by default, including &lt;code&gt;history.jsonl&lt;/code&gt; (&lt;a href="https://learn.chatgpt.com/docs/config-file/config-advanced" rel="noopener noreferrer"&gt;Codex docs&lt;/a&gt;), and writes session rollout files unless you run it non-interactively with &lt;code&gt;codex exec --ephemeral&lt;/code&gt; (&lt;a href="https://learn.chatgpt.com/docs/developer-commands" rel="noopener noreferrer"&gt;Codex commands&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rlF&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$K&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ~/.claude ~/.codex 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix for Claude Code: &lt;code&gt;claude purge ~/code/app --dry-run&lt;/code&gt; shows what it would delete for that project (transcripts, auto memory, matching lines in &lt;code&gt;history.jsonl&lt;/code&gt;), and the same command without &lt;code&gt;--dry-run&lt;/code&gt; asks once and deletes. &lt;code&gt;paste-cache/&lt;/code&gt; isn't on purge's list, and it leaves &lt;code&gt;backups/&lt;/code&gt; alone, so delete any files the grep found in those two by hand. Lower &lt;code&gt;cleanupPeriodDays&lt;/code&gt;, or set &lt;code&gt;CLAUDE_CODE_SKIP_PROMPT_HISTORY&lt;/code&gt; to stop writing transcripts and prompt history at all (&lt;a href="https://code.claude.com/docs/en/claude-directory" rel="noopener noreferrer"&gt;Claude Code docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;For Codex, &lt;code&gt;codex delete&lt;/code&gt; removes a session's transcript, while &lt;code&gt;codex archive&lt;/code&gt; only hides it from the picker (&lt;a href="https://learn.chatgpt.com/docs/developer-commands" rel="noopener noreferrer"&gt;Codex commands&lt;/a&gt;), and &lt;code&gt;persistence = "none"&lt;/code&gt; under &lt;code&gt;[history]&lt;/code&gt; in &lt;code&gt;config.toml&lt;/code&gt; stops &lt;code&gt;history.jsonl&lt;/code&gt; (&lt;a href="https://learn.chatgpt.com/docs/config-file/config-advanced" rel="noopener noreferrer"&gt;Codex docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Terminal scrollback.&lt;/strong&gt; If the agent ran &lt;code&gt;env&lt;/code&gt; or printed a config, the key sits in the scrollback of that pane. In &lt;a href="https://man.openbsd.org/tmux.1" rel="noopener noreferrer"&gt;tmux&lt;/a&gt;, &lt;code&gt;capture-pane -S -&lt;/code&gt; starts at the beginning of the pane's history and &lt;code&gt;-p&lt;/code&gt; prints it, so you can count hits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux capture-pane &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-S&lt;/span&gt; - &lt;span class="nt"&gt;-t&lt;/span&gt; &amp;lt;pane&amp;gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-cF&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$K&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
tmux clear-history &lt;span class="nt"&gt;-t&lt;/span&gt; &amp;lt;pane&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outside tmux, use your terminal's clear-scrollback command. Remember that anything the agent's tool printed is also in its transcript, which is item 2 again.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the repository: env files, fixtures, MCP configs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;4. A committed &lt;code&gt;.env&lt;/code&gt; or test fixture.&lt;/strong&gt; The agent wrote the key into a fixture to make the test pass, or a &lt;code&gt;.env&lt;/code&gt; got staged with everything else. &lt;code&gt;git log -S&lt;/code&gt; finds commits that add or remove an exact string (&lt;a href="https://git-scm.com/docs/git-log" rel="noopener noreferrer"&gt;git docs&lt;/a&gt;). &lt;a href="https://github.com/gitleaks/gitleaks" rel="noopener noreferrer"&gt;Gitleaks&lt;/a&gt; finds keys by pattern, including the ones you forgot: &lt;code&gt;gitleaks git&lt;/code&gt; scans history through &lt;code&gt;git log -p&lt;/code&gt;, and &lt;code&gt;gitleaks dir&lt;/code&gt; scans files on disk, so it catches the fixture you haven't committed yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$K&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt;
gitleaks git &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--log-opts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--all"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
gitleaks &lt;span class="nb"&gt;dir&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix: the old key is already revoked. If you still need it out of history, GitHub's guide uses &lt;code&gt;git-filter-repo --sensitive-data-removal&lt;/code&gt;, and warns that forks, other people's clones, cached views and pull requests that reference the commit can still hold it. GitHub Support removes cached views and PR references only after every other ref is clean, and only when rotating the credential can't mitigate the risk (&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository" rel="noopener noreferrer"&gt;GitHub Docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. An MCP server config.&lt;/strong&gt; &lt;code&gt;claude mcp add --header "Authorization: Bearer ..."&lt;/code&gt; with a literal token stores it in &lt;code&gt;~/.claude.json&lt;/code&gt; for local and user scope, and project scope writes &lt;code&gt;.mcp.json&lt;/code&gt;, which the docs tell you to commit (&lt;a href="https://code.claude.com/docs/en/mcp" rel="noopener noreferrer"&gt;Claude Code MCP docs&lt;/a&gt;). Codex reads MCP servers from &lt;code&gt;~/.codex/config.toml&lt;/code&gt; or a project's &lt;code&gt;.codex/config.toml&lt;/code&gt; (&lt;a href="https://learn.chatgpt.com/docs/extend/mcp" rel="noopener noreferrer"&gt;Codex MCP docs&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nF&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$K&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ~/.claude.json .mcp.json &lt;span class="se"&gt;\&lt;/span&gt;
  ~/.codex/config.toml .codex/config.toml 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code also keeps the five newest older copies of &lt;code&gt;~/.claude.json&lt;/code&gt; in &lt;code&gt;~/.claude/backups/&lt;/code&gt;, which item 2's grep already covers. Fix: reference variables instead of values. &lt;code&gt;.mcp.json&lt;/code&gt; expands &lt;code&gt;${API_KEY}&lt;/code&gt; in &lt;code&gt;headers&lt;/code&gt;, &lt;code&gt;env&lt;/code&gt; and &lt;code&gt;url&lt;/code&gt; (&lt;a href="https://code.claude.com/docs/en/mcp" rel="noopener noreferrer"&gt;Claude Code MCP docs&lt;/a&gt;), and Codex has &lt;code&gt;bearer_token_env_var&lt;/code&gt; and &lt;code&gt;env_vars&lt;/code&gt; to read a token from your environment rather than the file (&lt;a href="https://learn.chatgpt.com/docs/extend/mcp" rel="noopener noreferrer"&gt;Codex MCP docs&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  On GitHub: CI logs and screenshots
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;6. A CI log.&lt;/strong&gt; GitHub masks registered secrets in logs, but redaction "largely relies on finding an exact match," is "not guaranteed," and only covers secrets the job used (&lt;a href="https://docs.github.com/en/actions/reference/security/secure-use" rel="noopener noreferrer"&gt;GitHub Docs&lt;/a&gt;). A key the agent hard-coded into a fixture is not a registered secret, so a failing test that prints the request prints the key in full.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh run list
gh run view &amp;lt;run-id&amp;gt; &lt;span class="nt"&gt;--log&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-cF&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$K&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://cli.github.com/manual/gh_run_view" rel="noopener noreferrer"&gt;&lt;code&gt;gh run view --log&lt;/code&gt;&lt;/a&gt; prints the full log of a run. Fix: GitHub's advice when a secret shows up in a log is to delete the log and rotate the secret (&lt;a href="https://docs.github.com/en/actions/reference/security/secure-use" rel="noopener noreferrer"&gt;GitHub Docs&lt;/a&gt;). &lt;a href="https://cli.github.com/manual/gh_run_delete" rel="noopener noreferrer"&gt;&lt;code&gt;gh run delete &amp;lt;run-id&amp;gt;&lt;/code&gt;&lt;/a&gt; deletes the whole run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. A PR screenshot.&lt;/strong&gt; This is the one no grep finds. You pasted a screenshot of the failing request into the PR, and the header was in it. In public repositories, uploaded files "can be accessed without authentication"; in private ones, anyone with repo access can open them (&lt;a href="https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/attaching-files" rel="noopener noreferrer"&gt;GitHub Docs&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;view &amp;lt;number&amp;gt; &lt;span class="nt"&gt;--comments&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://cli.github.com/manual/gh_pr_view" rel="noopener noreferrer"&gt;&lt;code&gt;gh pr view --comments&lt;/code&gt;&lt;/a&gt; shows the pull request's comments, so you can open every image link and look. Fix: edit the comment to drop the image, then delete the old revision too, because anyone with read access can see a comment's edit history; open &lt;strong&gt;edited&lt;/strong&gt;, pick the revision, then &lt;strong&gt;Options&lt;/strong&gt; and "Delete revision from history" (&lt;a href="https://docs.github.com/en/communities/moderating-comments-and-conversations/tracking-changes-in-a-comment" rel="noopener noreferrer"&gt;GitHub Docs&lt;/a&gt;). The docs don't say editing deletes the uploaded file itself, which is one more reason to rotate.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the agent's own memory: rules and notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;8. A memory or rules file.&lt;/strong&gt; You told the agent "use this key for the staging tests," and it saved that as a note, or a teammate added it to the rules file so every session would have it. Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt; and &lt;code&gt;CLAUDE.local.md&lt;/code&gt; from the project (and can read &lt;code&gt;AGENTS.md&lt;/code&gt; in place of &lt;code&gt;CLAUDE.md&lt;/code&gt;), and keeps auto memory in &lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/memory/&lt;/code&gt;, which the 30-day sweep does not delete (&lt;a href="https://code.claude.com/docs/en/claude-directory" rel="noopener noreferrer"&gt;Claude Code docs&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnF&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$K&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; CLAUDE.md CLAUDE.local.md AGENTS.md &lt;span class="se"&gt;\&lt;/span&gt;
  .claude .codex 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Item 2's grep already walks &lt;code&gt;~/.claude&lt;/code&gt;, auto memory included. Fix: delete the line, and if the file is committed, it's item 4 again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Hermes IDE fits, and where it doesn't
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt;, so weigh this part with that in mind. It runs Claude Code, Codex and other CLI agents each in its own real terminal, and those terminals offer ghost-text suggestions from your command history as you type (&lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;README&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A history-based suggestion is one more place a key can come back. Since 2.3.2, released October 13, 2026, what you type at a hidden password prompt (sudo, ssh, a script asking for a passphrase) is never remembered or suggested. Commands that hold a secret, such as setting an API key or token, passing a password, an auth header or a URL with a password in it, are never shown as a suggestion or as faded text after the cursor (&lt;a href="https://github.com/hermes-hq/hermes-ide/releases" rel="noopener noreferrer"&gt;release notes&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5kjc5tovp3djubje324.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5kjc5tovp3djubje324.png" alt="A cream terminal window with a black prompt line, where the faded suggestion after the cursor is replaced by a small orange padlock instead of the hidden command" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Since 2.3.2, Hermes never offers a command that holds a secret back as a suggestion.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The same release notes say what it doesn't do: "Your shell's own history is not changed." Hermes keeps the key out of its suggestions, but &lt;code&gt;~/.zsh_history&lt;/code&gt; still has the line, so item 1 still applies to you. Hermes also has memory pins, facts saved for a project that reach every later session's agent, so the rule from item 8 applies there too: don't pin a key.&lt;/p&gt;

&lt;p&gt;It's free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Nous Research, Anthropic, OpenAI or any agent vendor. &lt;a href="https://hermes-ide.com/download" rel="noopener noreferrer"&gt;Download it here&lt;/a&gt;, or read the source on &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;If you're thinking about pointing a retrieval pipeline at your agent transcripts or your repo, everything on this list goes into the index with them, keys included. My &lt;em&gt;RAG Pocket Guide&lt;/em&gt; covers retrieval, chunking and reranking for production, so you decide what an index should hold before you build it. It's one of 8 short books in the &lt;em&gt;Pocket Guides for Developers&lt;/em&gt; series, covering agents, RAG, prompts, observability, events, databases and system design: &lt;a href="https://www.amazon.com/dp/B0GYLH38X4" rel="noopener noreferrer"&gt;the whole series is here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/dp/B0GX2YDC5Z" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2Fpkc0hvypembd4wgh6t0k.jpg" alt="RAG Pocket Guide: Retrieval, Chunking, and Reranking Patterns for Production" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>git</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
