<?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: vadim albarov</title>
    <description>The latest articles on DEV Community by vadim albarov (@vadim_albarov).</description>
    <link>https://dev.to/vadim_albarov</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%2F4058639%2Fae7afcec-637e-4f73-bea2-21b8cd0d9c4f.jpg</url>
      <title>DEV Community: vadim albarov</title>
      <link>https://dev.to/vadim_albarov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vadim_albarov"/>
    <language>en</language>
    <item>
      <title>The 49-Byte Field That Doubles Our SQS Bill</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Sun, 25 Oct 2026 15:55:49 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/the-49-byte-field-that-doubles-our-sqs-bill-2lod</link>
      <guid>https://dev.to/vadim_albarov/the-49-byte-field-that-doubles-our-sqs-bill-2lod</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Amazon SQS does not bill per message. It bills per &lt;em&gt;request&lt;/em&gt;, and every 64 KiB chunk of payload counts as one request. A message of 65,536 bytes is one request. A message of 65,537 bytes is two. If your producer packs messages tightly under that line (a lot of chunkers do exactly this, on purpose), then adding a harmless &lt;code&gt;traceId&lt;/code&gt; to the envelope doubles the billed requests for every send and every receive, and the total SQS bill goes up 67% to 95% depending on how you delete. Every price here comes from the AWS pricing page; the scenario is invented to show the mechanism. Here is how the cliff works, how to see whether you are standing on it, and how to make it a slope instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The rule nobody re-reads
&lt;/h2&gt;

&lt;p&gt;The SQS pricing page has one sentence that matters here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Each 64 KB chunk of a payload is billed as 1 request.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The example right below it says a 1 MiB payload is billed as 16 requests. 16 × 65,536 = 1,048,576, so "64 KB" means 64 KiB and the chunk is 65,536 bytes. AWS does not publish the exact boundary or which bytes it counts, so treat 65,536 as the implied line, not a documented one.&lt;/p&gt;

&lt;p&gt;Two more facts complete the picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Message attributes count.&lt;/strong&gt; The developer guide says all components of a message attribute (name, type, value) are included in the message size. Moving a field from the body to an attribute does not hide it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rule applies to every API action's payload.&lt;/strong&gt; &lt;code&gt;SendMessage&lt;/code&gt; is chunked on the way in. &lt;code&gt;ReceiveMessage&lt;/code&gt; is chunked on the way out, and its response also carries receipt handles and metadata, so a message near the line may already cost two on receive. &lt;code&gt;DeleteMessage&lt;/code&gt; carries only a receipt handle, so it stays at one request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One exception.&lt;/strong&gt; The X-Ray system attribute &lt;code&gt;AWSTraceHeader&lt;/code&gt; does not count toward message size. If your tracing is X-Ray, use it. The scenario below assumes it is not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prices themselves are small, which is exactly why nobody looks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Queue type&lt;/th&gt;
&lt;th&gt;Per million requests (first 100 billion, us-east-1)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;$0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIFO&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A scenario that is boring enough to be real
&lt;/h2&gt;

&lt;p&gt;Imagine a service that exports large reports through SQS. Years ago, someone read the pricing page and wrote a chunker: split the report into pieces, each sized to fit in 64 KiB, send each piece as one message. The constant is called &lt;code&gt;MAX_CHUNK_BYTES&lt;/code&gt; and it is set to &lt;code&gt;65_536&lt;/code&gt; minus the size of a small JSON envelope. There is no comment explaining the number.&lt;/p&gt;

&lt;p&gt;The chunker works. The event records are small, so every full chunk lands within a few dozen bytes of the line, between about 65,490 and 65,535 bytes. One request each. Nobody thinks about it again.&lt;/p&gt;

&lt;p&gt;Then a debugging ticket comes in. Cross-service tracing is painful, so the team adds a trace ID to the shared message envelope library. The change is one line in the serializer:&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="err"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"traceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"4f1c2b7e-9a3d-4e58-b2f0-6c8d1a9e3b47"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is 49 bytes. The PR is reviewed in four minutes. Tests pass. It ships.&lt;/p&gt;

&lt;p&gt;The chunker never sees this field. It sized the payload &lt;em&gt;before&lt;/em&gt; the envelope library serialized it. So every full chunk is now 65,539 to 65,584 bytes. All of them cross 65,536.&lt;/p&gt;

&lt;p&gt;What changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NumberOfMessagesSent&lt;/code&gt; in CloudWatch: &lt;strong&gt;unchanged&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Throughput, latency, error rate: &lt;strong&gt;unchanged&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Billed requests per message: send goes from 1 to 2, receive goes from 1 to 2, delete stays at 1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing in the usual dashboards moves. The bill does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The math
&lt;/h2&gt;

&lt;p&gt;Take a Standard queue, no batching, 1 billion chunk messages per month (about 390 per second, sustained). Consumers are EC2 or Lambda in the same region, so there is no data transfer charge. Ignore the free tier and empty receives.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Requests per message&lt;/th&gt;
&lt;th&gt;Requests per month&lt;/th&gt;
&lt;th&gt;Cost per month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before (send 1 + receive 1 + delete 1)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3 billion&lt;/td&gt;
&lt;td&gt;$1,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After (send 2 + receive 2 + delete 1)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5 billion&lt;/td&gt;
&lt;td&gt;$2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is +$800 per month, $9,600 per year, for 49 bytes nobody needed in production. On a high-throughput FIFO queue it is $1,500 to $2,500 (a default FIFO queue caps at 300 unbatched requests per second per partition, so this workload would not fit without batching).&lt;/p&gt;

&lt;p&gt;Notice the total is +67%, not +100%. The send and the receive did double. The delete did not, because it carries only a receipt handle, and it dilutes the ratio. You cannot skip it: an undeleted message comes back after the visibility timeout and gets received, and billed, again.&lt;/p&gt;

&lt;p&gt;Most consumers delete in batches, though. With &lt;code&gt;DeleteMessageBatch&lt;/code&gt; at 10 per call the delete almost disappears from the math, and the total goes from 2.1 to 4.1 requests per message, +95%. So the honest range is "the bill goes up 67% to 95%, depending on how you delete", and the honest headline is "the cost of every message you send and receive doubles". If you say "doubled" in a meeting, be ready to say which one you mean.&lt;/p&gt;

&lt;p&gt;Also notice the shape of the function. The cost per message is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size_in_bytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a staircase. Between steps it is free to grow. At each step it jumps. The 49-byte field was expensive only because the messages were already standing on the edge of a step. The same 49 bytes added to 20 KiB messages cost nothing at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to know if you are on the edge
&lt;/h2&gt;

&lt;p&gt;You cannot see this from message counts. You need sizes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudWatch, per queue.&lt;/strong&gt; The &lt;code&gt;SentMessageSize&lt;/code&gt; metric has Average, Maximum, and SampleCount. Plot Maximum and Average against a horizontal line at 65,536. If the Average sits within a few hundred bytes under the line, you have a packer, and you are on the edge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost Explorer.&lt;/strong&gt; Filter to SQS, group by usage type. The line is &lt;code&gt;Requests-Tier1&lt;/code&gt; or &lt;code&gt;Requests-RBP&lt;/code&gt; (FIFO has its own variants, and regions outside us-east-1 add a prefix). Divide monthly billed requests by &lt;code&gt;NumberOfMessagesSent&lt;/code&gt; summed over your queues, after subtracting &lt;code&gt;NumberOfEmptyReceives&lt;/code&gt;. For an unbatched workload with small messages the ratio is about 3. If it is about 5, your messages are between 64 KiB and 128 KiB and nobody told you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One-off check.&lt;/strong&gt; Pull 1,000 messages with &lt;code&gt;ReceiveMessage&lt;/code&gt; and a short visibility timeout, dedupe on &lt;code&gt;MessageId&lt;/code&gt;, measure the UTF-8 byte length of body plus attributes, and histogram it. A spike just under 65,536 is the signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the cliff a slope
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Batch.&lt;/strong&gt; This is the one people underestimate. AWS's wording applies the chunk rule to the payload of the &lt;em&gt;API action&lt;/em&gt;, and a &lt;code&gt;SendMessageBatch&lt;/code&gt; call holds up to 10 messages and up to 1 MiB in total. Read literally, the batch is summed and then rounded. Ten messages of 65,500 bytes are 655,000 bytes, which is 10 chunks. Ten messages of 65,549 bytes are 655,490 bytes, which is 11 chunks. AWS does not state the sum-then-round behaviour outright, so check it against your own bill before you depend on it. The same change that doubled the unbatched send cost adds 10% to the batched one. Batching does not just reduce request count. It averages out the rounding.&lt;/p&gt;

&lt;p&gt;Same scenario, batched 10 at a time for send, receive, and delete:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Requests per month&lt;/th&gt;
&lt;th&gt;Cost per month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before&lt;/td&gt;
&lt;td&gt;2.1 billion&lt;/td&gt;
&lt;td&gt;$840&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After&lt;/td&gt;
&lt;td&gt;2.3 billion&lt;/td&gt;
&lt;td&gt;$920&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;+$80 instead of +$800.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compress.&lt;/strong&gt; SQS does not compress for you. It stores the bytes you send and bills on them. But JSON report chunks typically gzip 3x to 10x in application code. One catch: the message body must be valid text, so raw gzip output has to be base64-encoded, which adds about a third to the compressed size. A 65 KiB chunk still ends up around 15 to 25 KiB. A &lt;code&gt;Binary&lt;/code&gt; message attribute with a one-character body avoids base64 in your code, but AWS does not document whether it bills the raw or the encoded bytes, so do not count on it saving money. Either way, mark compressed messages with an attribute like &lt;code&gt;contentEncoding&lt;/code&gt; so consumers can handle both formats during rollout.&lt;/p&gt;

&lt;p&gt;What does it cost in CPU? I measured a 65 KB JSON chunk with CPython's zlib on a desktop CPU:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gzip level&lt;/th&gt;
&lt;th&gt;Size after gzip + base64&lt;/th&gt;
&lt;th&gt;Compress&lt;/th&gt;
&lt;th&gt;Decompress&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;18 KB (3.6x smaller)&lt;/td&gt;
&lt;td&gt;0.6 ms&lt;/td&gt;
&lt;td&gt;0.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;14 KB (4.6x smaller)&lt;/td&gt;
&lt;td&gt;1.4 ms&lt;/td&gt;
&lt;td&gt;0.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;13.5 KB (4.8x smaller)&lt;/td&gt;
&lt;td&gt;6.1 ms&lt;/td&gt;
&lt;td&gt;0.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Level 9 is not worth it. At level 1 and 1 billion messages a month, the producer needs about a quarter of a core on average and under one core at a daytime peak, and the consumer a few percent of a core. Call it 1 to 2 vCPUs on cloud hardware, about $60 a month, against an $800 a month increase. Latency per message goes up by about a millisecond, which is noise next to a 10 to 30 ms HTTPS round trip.&lt;/p&gt;

&lt;p&gt;And once chunks are 14 KB, batching works again: ten compressed chunks are 140 KB, which is 3 billed chunks instead of 10. Compressed and batched, the same workload is about 0.7 billion requests a month, or $280. That is below where you started, not just back to it. You are now nowhere near a step, and you can add all the debug fields you like for years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Size the envelope, not the body.&lt;/strong&gt; If you keep a chunker, make it measure the final serialized message, after every library has had its turn. Better: make it a test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_chunk_fits_in_one_billing_unit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_largest_possible_chunk&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;wire&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# body + attributes, as sent
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wire&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;65_536&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;512&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;SQS bills per 64 KiB chunk; see docs/sqs-billing.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The margin and the assertion message are the point. The original author of &lt;code&gt;MAX_CHUNK_BYTES&lt;/code&gt; knew the rule. The test is how the next person finds out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claim check.&lt;/strong&gt; If chunks are large because the payload is large, put the payload in S3 and send a pointer. The SQS Extended Client Library (Java and Python) does this for you. It was the standard answer before the limit went to 1 MiB in August 2025, and it is still the answer above 1 MiB.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would actually take from this
&lt;/h2&gt;

&lt;p&gt;The uncomfortable part of the scenario is not the rule. It is that the system had the knowledge once, encoded it as a number, and lost the reason. A field that costs nothing to add, nothing to run, and nothing to test can still cost $9,600 a year, and the only place it shows up is a line in Cost Explorer that someone checks quarterly.&lt;/p&gt;

&lt;p&gt;If you run SQS at volume, spend ten minutes this week: open &lt;code&gt;SentMessageSize&lt;/code&gt; for your busiest queues and look at where the Average sits relative to 65,536. If it is close, you have found either a careful engineer from years ago or a bill waiting to happen. Usually both.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>discuss</category>
      <category>debugging</category>
    </item>
    <item>
      <title>When Does Claude Code Check the Web? I Logged My Sessions to Find Out</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Thu, 15 Oct 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/when-does-claude-code-check-the-web-i-logged-my-sessions-to-find-out-3ki8</link>
      <guid>https://dev.to/vadim_albarov/when-does-claude-code-check-the-web-i-logged-my-sessions-to-find-out-3ki8</guid>
      <description>&lt;p&gt;On October 12, 2026, AWS launched Lambda Web Functions. You can now run a normal Express or Next.js app on Lambda, with up to 128 concurrent requests per execution environment.&lt;/p&gt;

&lt;p&gt;Two days later I asked three Claude models in Claude Code: "Our Express app is in app.js. Add the files needed to deploy it to AWS Lambda with the fewest moving parts."&lt;/p&gt;

&lt;p&gt;All six runs wrote a setup that was the best answer until October 11: &lt;code&gt;serverless-http&lt;/code&gt;, &lt;code&gt;serverless-express&lt;/code&gt;, or the Lambda Web Adapter. Five of them looked nothing up. The sixth only checked the latest version of &lt;code&gt;serverless-http&lt;/code&gt; on npm.&lt;/p&gt;

&lt;p&gt;Then I asked "What's the newest way to run an Express app on AWS Lambda, as of this month?" Every run searched the web. Five of six named Web Functions.&lt;/p&gt;

&lt;p&gt;The topic was the same. Only the wording changed. I wanted to know how far that goes. When does Claude Code look things up, and when does it answer from its training data? And when it answers from training data, how often is the answer out of date?&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Seven topics that changed after the models' training cutoff&lt;/strong&gt; (June 2026). I checked each one against the official source on the day of the test:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Current fact&lt;/th&gt;
&lt;th&gt;Since&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lambda Web Functions&lt;/td&gt;
&lt;td&gt;new Lambda form factor for HTTP apps&lt;/td&gt;
&lt;td&gt;2026-10-12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda Python runtimes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;python3.14&lt;/code&gt; GA, &lt;code&gt;python3.15&lt;/code&gt; in preview&lt;/td&gt;
&lt;td&gt;2025-11 / 2026-08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actions/setup-node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;v7&lt;/td&gt;
&lt;td&gt;2026-07-14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openai Python SDK&lt;/td&gt;
&lt;td&gt;3.x, built on HTTPX2, no longer installs &lt;code&gt;httpx&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2026-08-12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe API version&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-09-30.endive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-09-30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Django&lt;/td&gt;
&lt;td&gt;6.1&lt;/td&gt;
&lt;td&gt;2026-08-05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;3.15.0&lt;/td&gt;
&lt;td&gt;2026-10-09&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Three ways to ask about each topic:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Neutral question&lt;/strong&gt;: "Which major version of actions/setup-node should a GitHub workflow use?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time cue&lt;/strong&gt;: "What's the latest major version of actions/setup-node today?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coding task&lt;/strong&gt;: "Add a GitHub Actions workflow that runs npm test on every push."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus three &lt;strong&gt;stable controls&lt;/strong&gt;, like "What does HTTP status 409 mean?"&lt;/p&gt;

&lt;p&gt;That makes 24 prompts. I ran them on Haiku 5.5, Sonnet 5.5, and Opus 5.5, two trials each: 144 sessions. Each one was a fresh headless session in a small repo (an Express app, a Python Lambda handler, a README):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;prompt&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; &amp;lt;model&amp;gt; &lt;span class="nt"&gt;--output-format&lt;/span&gt; stream-json &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; 10 &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; acceptEdits &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Glob,Grep,Edit,Write,WebFetch,WebSearch,Bash,PowerShell,ToolSearch"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before each run, the repo is reset with git and there is no project memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What counts as a web call:&lt;/strong&gt; &lt;code&gt;WebFetch&lt;/code&gt;, &lt;code&gt;WebSearch&lt;/code&gt;, and shell commands that reach the network. That last group matters. Claude often skips WebFetch and calls &lt;code&gt;gh api&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt; against the PyPI JSON API, &lt;code&gt;pip index versions&lt;/code&gt;, or &lt;code&gt;git ls-remote --tags&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grading.&lt;/strong&gt; A Sonnet subagent graded each of the 126 non-control answers against the ground truth table, as one of three grades:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;current&lt;/strong&gt;: uses the current fact as a fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hedged&lt;/strong&gt;: old or unsure, but tells the user it may be out of date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stale&lt;/strong&gt;: presents old information as current.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For coding tasks it also graded the file the model wrote. I checked the grades by hand and changed four of them (see Limits).&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 1: the trigger is the words, not the topic
&lt;/h2&gt;

&lt;p&gt;Share of runs with at least one web call:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Stable control&lt;/th&gt;
&lt;th&gt;Neutral question&lt;/th&gt;
&lt;th&gt;Coding task&lt;/th&gt;
&lt;th&gt;Time cue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 5.5&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;td&gt;8/14&lt;/td&gt;
&lt;td&gt;5/14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14/14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;td&gt;7/14&lt;/td&gt;
&lt;td&gt;4/14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14/14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;td&gt;12/14&lt;/td&gt;
&lt;td&gt;7/14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14/14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Latest", "today", "right now", "as of this month": 42 out of 42 runs looked it up. Stable questions: 0 out of 18. That part works as you would hope.&lt;/p&gt;

&lt;p&gt;The middle columns are where it gets interesting. Within them, the pattern depends on &lt;em&gt;what kind&lt;/em&gt; of question it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Which version should I use?"&lt;/strong&gt; usually triggers a lookup. For setup-node, Lambda Python runtimes, and Stripe API versions, 18 of 18 neutral runs checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"How do I use this API?"&lt;/strong&gt; never does. "How do I pass a custom httpx client with a proxy to the OpenAI Python SDK?" got 0 lookups in 6 runs. None mentioned that version 3 exists. Five of six used &lt;code&gt;httpx.Client&lt;/code&gt;, which fails on a fresh install of the current SDK (details below). One Opus run used the SDK's &lt;code&gt;DefaultHttpxClient&lt;/code&gt;, which still works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Can this service do X?"&lt;/strong&gt; depends on the model. "Can AWS Lambda serve many concurrent HTTP requests from one execution environment?" Haiku and Sonnet answered from memory every time ("no, one request per environment"). Opus searched both times. More on what it found below.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Finding 2: no lookup means a stale answer, and usually no warning
&lt;/h2&gt;

&lt;p&gt;Here is every graded answer, split by whether the session made a web call:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Runs&lt;/th&gt;
&lt;th&gt;Current&lt;/th&gt;
&lt;th&gt;Hedged&lt;/th&gt;
&lt;th&gt;Stale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;With a web call&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;69 (81%)&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Without a web call&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;3 (7%)&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;31 (76%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three "current" answers without a web call were all Opus on the OpenAI topic. Each used the SDK's own &lt;code&gt;DefaultHttpxClient&lt;/code&gt; helper, which version 3 still supports. In the two coding runs, Opus also ran the code against the installed package, which happened to be the current version. None of the three knew that version 3 exists. The code worked because the SDK kept an old name. Apart from that one case, no answer from training data alone was current.&lt;/p&gt;

&lt;p&gt;That is expected for facts that changed after the cutoff. What I didn't expect was how rarely the models said so. Of the 35 stale answers, &lt;strong&gt;28 gave no warning at all&lt;/strong&gt;. The other 7 had some caveat, but often a narrow or generic one. For example, one answer warned about checking the Lambda Web Adapter layer version, but it didn't know Web Functions existed.&lt;/p&gt;

&lt;p&gt;When Sonnet did hedge, the hedges were good ones: "Django 6.1 if it is out, I'm not sure it has shipped", "I answered from memory and did not check". When it hedged, it meant it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3: coding tasks are the blind spot
&lt;/h2&gt;

&lt;p&gt;Here is what ended up in the files for the coding tasks that did not mention "newest":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Web lookups&lt;/th&gt;
&lt;th&gt;What was written (6 runs)&lt;/th&gt;
&lt;th&gt;Current&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub workflow running &lt;code&gt;npm test&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;setup-node@v4&lt;/code&gt; (Haiku, Sonnet), &lt;code&gt;@v5&lt;/code&gt; (Opus)&lt;/td&gt;
&lt;td&gt;v7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAM template for a Python Lambda&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;python3.13&lt;/code&gt; (5 runs), &lt;code&gt;python3.12&lt;/code&gt; (1 run)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python3.14&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI client through a proxy&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;import httpx&lt;/code&gt; + &lt;code&gt;httpx.Client(proxy=...)&lt;/code&gt; (Haiku, Sonnet), &lt;code&gt;DefaultHttpxClient&lt;/code&gt; (Opus)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DefaultHttpx2Client&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Express app on Lambda&lt;/td&gt;
&lt;td&gt;1/6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;serverless-http&lt;/code&gt;, &lt;code&gt;serverless-express&lt;/code&gt;, Lambda Web Adapter&lt;/td&gt;
&lt;td&gt;Web Functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe client with a pinned API version&lt;/td&gt;
&lt;td&gt;3/6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2026-09-30.endive&lt;/code&gt; (3), &lt;code&gt;2025-09-30.clover&lt;/code&gt;, &lt;code&gt;2025-03-31.basil&lt;/code&gt;, &lt;code&gt;2024-06-20&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-09-30.endive&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Stripe row shows it best. Opus searched in both runs and pinned the current version. Haiku searched in one of two runs and got it right in that one. Sonnet never searched and pinned versions from 2025.&lt;/p&gt;

&lt;p&gt;Two coding tasks did trigger lookups every time: "pin Django to the &lt;strong&gt;newest&lt;/strong&gt; stable version" and "a Dockerfile on the &lt;strong&gt;newest&lt;/strong&gt; stable Python". 12 out of 12. The word "newest" in the coding task was enough.&lt;/p&gt;

&lt;p&gt;The OpenAI row needs a note, because "outdated" can mean "still works". I checked it in a clean virtual environment with only &lt;code&gt;openai==3.31.0&lt;/code&gt; installed. The Haiku and Sonnet code fails on its first line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ModuleNotFoundError: No module named 'httpx'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;openai 3.x depends on &lt;code&gt;httpx2&lt;/code&gt; and no longer pulls in &lt;code&gt;httpx&lt;/code&gt;. In an older environment that still has &lt;code&gt;httpx&lt;/code&gt;, the same code runs. The Opus version uses the SDK's own &lt;code&gt;DefaultHttpxClient&lt;/code&gt;, which 3.x keeps as an alias built on &lt;code&gt;httpx2&lt;/code&gt;. That one works on a fresh install too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 4: the training data is older than the cutoff
&lt;/h2&gt;

&lt;p&gt;These models have a June 2026 training cutoff. But look at what the coding tasks wrote:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda added &lt;code&gt;python3.14&lt;/code&gt; on &lt;strong&gt;November 18, 2025&lt;/strong&gt;, seven months before the cutoff. All six SAM templates used 3.13 or 3.12.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;actions/setup-node&lt;/code&gt; v6 shipped in &lt;strong&gt;October 2025&lt;/strong&gt;. Haiku and Sonnet wrote v4 (2023). Opus wrote v5 (September 2025).&lt;/li&gt;
&lt;li&gt;Haiku pinned Stripe API version &lt;code&gt;2024-06-20&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "the model knows things up to its cutoff" is too generous for coding defaults. My guess at the reason: when a model writes a version from memory, it writes the version it saw most in training. That is the version most existing code used, often a year or two behind the cutoff. The newest versions are thin in the training data, because little code used them yet.&lt;/p&gt;

&lt;p&gt;The practical effect: checking whether something changed after the cutoff is not enough. A version pinned from memory can be stale even for things released well before it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 5: a lookup is not the same as a current answer
&lt;/h2&gt;

&lt;p&gt;Four stale answers came &lt;em&gt;after&lt;/em&gt; a web call. They show two different ways a search can fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The search asked about what the model already knew.&lt;/strong&gt; On "Can AWS Lambda serve many concurrent HTTP requests from one execution environment?", Opus searched both times. Both times the query was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Lambda Managed Instances multi-concurrency execution environment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lambda Managed Instances is a real feature from late 2025. Opus knew it from training, searched to confirm it, and got a good, sourced answer about Managed Instances. It never found Web Functions, launched two days earlier, which is the direct answer to the question. The search was built from the model's memory, so it could only confirm that memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The search index was behind.&lt;/strong&gt; Python 3.15.0 came out on October 9. On October 14, Sonnet searched "latest stable Python release python.org downloads" and got cached snapshots of python.org listing 3.14.8 as the newest release and 3.15 as a pre-release. It wrote &lt;code&gt;python:3.14-slim&lt;/code&gt; into the Dockerfile. In a different run, the query "latest stable Python release version October 2026" found 3.15. The search with the month in it worked. The one without it got old snapshots.&lt;/p&gt;

&lt;p&gt;To be fair, Sonnet said it got the version from a web search. And when asked the plain question ("What is the newest stable Python release today?"), it got the same stale snapshots. Both times it said the search results disagreed, and once it added that 3.15 "may have shipped since". It was honest about the gap. In the Dockerfile run, it still wrote the old version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 6: one line in CLAUDE.md fixes most of it
&lt;/h2&gt;

&lt;p&gt;If the gap is that coding tasks don't trigger lookups, the obvious fix is to tell the model to look things up. I added this &lt;code&gt;CLAUDE.md&lt;/code&gt; to the repo and reran the seven coding tasks, 3 models x 2 trials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your training data may be out of date. Before you write any version number,
runtime identifier, GitHub Action version, API version, or SDK usage pattern
into a file, look up the current value (web, package registry, or official
docs). Prefer the newest stable option unless the repo pins something else.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coding tasks with a web lookup went from &lt;strong&gt;16 of 42 runs to 42 of 42&lt;/strong&gt;. Answers graded current went from &lt;strong&gt;15 of 42 to 33 of 42&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Per task, files with the current value (6 runs each):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Without CLAUDE.md&lt;/th&gt;
&lt;th&gt;With CLAUDE.md&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub workflow (&lt;code&gt;setup-node&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;6/6&lt;/strong&gt; (&lt;code&gt;@v7&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAM template (Python runtime)&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;6/6&lt;/strong&gt; (&lt;code&gt;python3.14&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe client (API version)&lt;/td&gt;
&lt;td&gt;3/6&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;6/6&lt;/strong&gt; (&lt;code&gt;2026-09-30.endive&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI client through a proxy&lt;/td&gt;
&lt;td&gt;2/6 work&lt;/td&gt;
&lt;td&gt;4/6 (&lt;code&gt;DefaultHttpx2Client&lt;/code&gt;), Haiku still wrote &lt;code&gt;import httpx&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Django requirements ("newest")&lt;/td&gt;
&lt;td&gt;6/6&lt;/td&gt;
&lt;td&gt;6/6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dockerfile ("newest" Python)&lt;/td&gt;
&lt;td&gt;5/6&lt;/td&gt;
&lt;td&gt;5/6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Express app on Lambda&lt;/td&gt;
&lt;td&gt;0/6&lt;/td&gt;
&lt;td&gt;0/6 (Opus found Web Functions, then ran out of turns)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Version pins are fixed. The two tasks that already said "newest" didn't change, because they were already looking things up.&lt;/p&gt;

&lt;p&gt;Two things the rule did not fully fix:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Haiku looked up the version, not the API.&lt;/strong&gt; On the OpenAI task, Haiku checked the latest version on PyPI both times. It then wrote the same &lt;code&gt;import httpx&lt;/code&gt; code as before. It treated "look up" as "find the version number", not "find out how the SDK is used now". Sonnet and Opus read the SDK README and wrote &lt;code&gt;DefaultHttpx2Client&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The approach changed only for the models that read past the version number.&lt;/strong&gt; Haiku and Sonnet used the rule to look up versions &lt;em&gt;for the approach they had already chosen from memory&lt;/em&gt;: the latest &lt;code&gt;serverless-http&lt;/code&gt; on npm, the latest Lambda Web Adapter layer, the latest Node runtime. One Sonnet run also opened the AWS page on Web Functions runtimes. It then named Web Functions as an alternative and explained why it didn't use them: &lt;code&gt;app.js&lt;/code&gt; would need a small change, and it wasn't sure SAM supports them yet. That's a reasonable call. The other Haiku and Sonnet runs never mentioned Web Functions.&lt;/p&gt;

&lt;p&gt;Opus went further. In both runs, the Lambda runtimes page led it to Web Functions. It then read the Web Functions runtimes page, the samples, the "create" guide, and the &lt;code&gt;aws lambda-web deploy&lt;/code&gt; CLI reference. It was clearly heading for the new approach. Then both runs hit my 10-turn limit before writing a file. So I can't say what it would have written, only that the rule made it find the feature that launched two days earlier.&lt;/p&gt;

&lt;p&gt;The cost of the rule: across the 42 coding runs, the total went from $2.51 to $4.47 (+78%), and Sonnet's average time went from 10 to 18 seconds. For setup work that pins versions, that's cheap. For every small edit, you may not want it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The models compared
&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;Avg web calls per run&lt;/th&gt;
&lt;th&gt;Coding tasks graded current&lt;/th&gt;
&lt;th&gt;Cost for 48 runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 5.5&lt;/td&gt;
&lt;td&gt;1.08&lt;/td&gt;
&lt;td&gt;4/14&lt;/td&gt;
&lt;td&gt;$0.57&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;td&gt;3/14&lt;/td&gt;
&lt;td&gt;$2.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5&lt;/td&gt;
&lt;td&gt;1.33&lt;/td&gt;
&lt;td&gt;8/14&lt;/td&gt;
&lt;td&gt;$5.68&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Opus looks things up the most, mostly on neutral questions (12 of 14 against 7 or 8 for the others), and it is the only one that searched on the Stripe coding task both times. Haiku searched more than Sonnet. Sonnet was the most likely to answer from memory, and also the most likely to say so.&lt;/p&gt;

&lt;p&gt;Costs are the &lt;code&gt;total_cost_usd&lt;/code&gt; Claude Code reports per session. The whole experiment cost about $8.30 for the 144 main runs, plus $4.50 for the 42 CLAUDE.md runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two trials per model.&lt;/strong&gt; The patterns were very stable across trials, but rare behavior won't show up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seven topics.&lt;/strong&gt; All are developer tooling with a version number or a launch date. I didn't test topics where "what changed" is fuzzier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless sessions&lt;/strong&gt; with &lt;code&gt;acceptEdits&lt;/code&gt;. An interactive session may behave a bit differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One grader&lt;/strong&gt;, a Sonnet subagent, with my spot checks. I changed four grades in two cases. One rewarded a generic "AWS may have changed" footer. In the other three, old-style OpenAI code turned out to still work on the new SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 10-turn cap.&lt;/strong&gt; Both Opus runs of the Express task with the CLAUDE.md rule hit it while reading the Web Functions docs, so they wrote no files. With a higher cap, that row would probably look better for Opus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The test date was close to the launches on purpose.&lt;/strong&gt; Lambda Web Functions were two days old. That is the hardest case, and the most realistic one for "what changed last week".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I take from this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Time words are the trigger.&lt;/strong&gt; "Latest", "newest", "current", "today": with them, all three models looked things up in every run. Without them, the model mostly answers from memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coding tasks don't trigger lookups.&lt;/strong&gt; "Write a workflow", "write a SAM template", "write a Stripe client" all pin a version, and the model picks it from memory. Most of the stale values in this test were written into files, not stated in answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How do I use this API?" is the hardest case.&lt;/strong&gt; It doesn't sound time-sensitive, so no model checked. But SDKs change their APIs, and the answer is the old API with no warning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the version pins in every diff.&lt;/strong&gt; Actions versions, runtime identifiers, API version strings, base image tags. These are the lines most likely to come from old training data, and the easiest to check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put the rule in CLAUDE.md.&lt;/strong&gt; One paragraph took version pins in coding tasks from 0 of 6 to 6 of 6 for setup-node and the Lambda runtime. It costs extra lookups on every coding task. It fixes versions reliably. It changes the approach only when the model reads beyond the version number, which Opus did and Haiku did not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you need the newest thing, say "newest", and name the month if it's recent.&lt;/strong&gt; "Newest stable Python" triggered a lookup every time. "October 2026" in the query got past stale search snapshots. And if you know a feature launched last week, name it. A search built from the model's memory can only confirm that memory.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>javascript</category>
      <category>claude</category>
    </item>
    <item>
      <title>A Reader Asked Whether a Hotter Rewriter Washes Out SynthID Faster. I Ran the Sweep.</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Wed, 14 Oct 2026 16:10:47 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/a-reader-asked-whether-a-hotter-rewriter-washes-out-synthid-faster-i-ran-the-sweep-433m</link>
      <guid>https://dev.to/vadim_albarov/a-reader-asked-whether-a-hotter-rewriter-washes-out-synthid-faster-i-ran-the-sweep-433m</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/vadim_albarov/i-watermarked-100-texts-with-synthid-and-asked-local-llms-to-wash-it-out-the-detector-still-saw-500i"&gt;SynthID post&lt;/a&gt; I rewrote 100 watermarked texts with three local models, and the detector still saw a trace in most of them. Every rewriter ran at one sampling setting: temperature 0.7, with Ollama's defaults of top-p 0.9 and top-k 40. I never varied it.&lt;/p&gt;

&lt;p&gt;A reader asked the obvious question. SynthID lives in exact token sequences: the watermark nudges each token based on the 4 tokens before it, so a rewrite carries watermark only where it reproduces a 5-token window from the original. Would a hotter rewriter shatter those windows faster? Or would it just shatter the meaning faster?&lt;/p&gt;

&lt;p&gt;I did not know. So I ran it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; both, in that order. Turning the temperature up to 1.3 while keeping the usual nucleus cut nearly halved the number of rewrites the detector flagged, and a blind judge could not tell the difference in meaning or readability. Removing the nucleus cut as well removed only a little more watermark, and that is where the text broke: a quarter of those rewrites lost or distorted facts. Greedy decoding went the other way and kept &lt;em&gt;more&lt;/em&gt; watermark. And nothing got to zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the sampling knob should matter
&lt;/h2&gt;

&lt;p&gt;Two things decide how much of the original's wording a rewriter reuses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temperature&lt;/strong&gt; sets how strongly the rewriter prefers its most probable next token. Low temperature means the most natural phrasing, and the most natural phrasing is often what the original model wrote too. That was the lesson of the second-pass result in the first post, where paraphrasing a paraphrase landed back on the original's phrases. A hotter rewriter should land on them less often.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top-p&lt;/strong&gt;, the nucleus cut, decides which tokens can be sampled at all. At top-p 0.9 the rewriter only ever picks from the plausible set, however hot the temperature. At top-p 1.0 the whole tail is open, including tokens the model thinks are unlikely.&lt;/p&gt;

&lt;p&gt;In the first post, the share of the original's 5-grams that survived a rewrite tracked the watermark z-score with a correlation of 0.77. So the prediction is simple: anything that lowers 5-gram overlap should lower the signal. The question is what it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rewriter:&lt;/strong&gt; &lt;code&gt;gemma3:12b&lt;/code&gt;, the heaviest washer from the first post. Same prompt, same seed, same context length. Only the sampling options changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five settings:&lt;/strong&gt; temperature 0 (greedy), 0.7 (the original run, used as the baseline), 1.0, 1.3, and 1.3 with top-p raised to 1.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Texts:&lt;/strong&gt; the same 100 watermarked originals, 190 to 900 words across 10 genres. Each setting also rewrote the 100 &lt;em&gt;clean&lt;/em&gt; originals, so I can check that the detector's null does not move when the rewriter gets hotter. That was a lesson from the readers of the first post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detector and cutoffs:&lt;/strong&gt; unchanged from the October update. The z cutoff of 2.42 was set on a calibration half of 850 clean texts for a 1% false-positive rate. Nothing was recalibrated for this test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparisons:&lt;/strong&gt; every setting rewrote the same 100 originals, so differences from the baseline are paired by source text, with bootstrap intervals over the 100 sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meaning:&lt;/strong&gt; 500 original-and-rewrite pairs, shuffled into 25 batches of 20 with the settings mixed, graded by a Claude judge on meaning (1 to 5: facts, names, numbers, steps and plot events preserved, nothing invented) and fluency (1 to 5). The judge never saw which setting produced a rewrite.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rewriter setting&lt;/th&gt;
&lt;th&gt;5-grams kept&lt;/th&gt;
&lt;th&gt;Mean z&lt;/th&gt;
&lt;th&gt;Flagged (mean test)&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;th&gt;AUC vs clean&lt;/th&gt;
&lt;th&gt;AUC vs rewritten null&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;temp 0 (greedy)&lt;/td&gt;
&lt;td&gt;12.7%&lt;/td&gt;
&lt;td&gt;1.78&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;31%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;23 to 41&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temp 0.7 (first post)&lt;/td&gt;
&lt;td&gt;10.4%&lt;/td&gt;
&lt;td&gt;1.56&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;18 to 34&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temp 1.0&lt;/td&gt;
&lt;td&gt;9.3%&lt;/td&gt;
&lt;td&gt;1.38&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;td&gt;14 to 30&lt;/td&gt;
&lt;td&gt;0.77&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temp 1.3&lt;/td&gt;
&lt;td&gt;8.2%&lt;/td&gt;
&lt;td&gt;1.08&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9 to 22&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temp 1.3, top-p 1.0&lt;/td&gt;
&lt;td&gt;5.9%&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6 to 19&lt;/td&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Paired against the 0.7 baseline over the same 100 texts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rewriter setting&lt;/th&gt;
&lt;th&gt;Change in z&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;th&gt;Change in 5-grams kept&lt;/th&gt;
&lt;th&gt;Meaning (1 to 5)&lt;/th&gt;
&lt;th&gt;Change in meaning&lt;/th&gt;
&lt;th&gt;Fluency (1 to 5)&lt;/th&gt;
&lt;th&gt;Change in fluency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;temp 0&lt;/td&gt;
&lt;td&gt;+0.22&lt;/td&gt;
&lt;td&gt;-0.01 to +0.46&lt;/td&gt;
&lt;td&gt;+2.3 pts&lt;/td&gt;
&lt;td&gt;4.26&lt;/td&gt;
&lt;td&gt;+0.07 (-0.06 to +0.19)&lt;/td&gt;
&lt;td&gt;4.92&lt;/td&gt;
&lt;td&gt;-0.03 (-0.09 to +0.03)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temp 0.7&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;4.19&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;4.95&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temp 1.0&lt;/td&gt;
&lt;td&gt;-0.18&lt;/td&gt;
&lt;td&gt;-0.44 to +0.08&lt;/td&gt;
&lt;td&gt;-1.1 pts&lt;/td&gt;
&lt;td&gt;4.22&lt;/td&gt;
&lt;td&gt;+0.03 (-0.09 to +0.15)&lt;/td&gt;
&lt;td&gt;4.90&lt;/td&gt;
&lt;td&gt;-0.05 (-0.11 to +0.01)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temp 1.3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-0.48&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-0.72 to -0.23&lt;/td&gt;
&lt;td&gt;-2.1 pts&lt;/td&gt;
&lt;td&gt;4.11&lt;/td&gt;
&lt;td&gt;-0.08 (-0.19 to +0.03)&lt;/td&gt;
&lt;td&gt;4.93&lt;/td&gt;
&lt;td&gt;-0.02 (-0.09 to +0.05)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temp 1.3, top-p 1.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-0.67&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-0.99 to -0.36&lt;/td&gt;
&lt;td&gt;-4.4 pts&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.82&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;-0.37&lt;/strong&gt; (-0.50 to -0.24)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.32&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;-0.63&lt;/strong&gt; (-0.77 to -0.49)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Greedy decoding keeps more watermark.&lt;/strong&gt; At temperature 0 the rewriter kept 12.7% of the original's 5-grams instead of 10.4%, and the flagged rate went up from 25% to 31%. The z change is +0.22 with an interval that just touches zero, so call it a lean rather than a proof. But the direction is the second-pass effect again: the most probable phrasing is the original's phrasing, and the watermark comes with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Up to the nucleus cut, temperature is a free knob.&lt;/strong&gt; At 1.3 with the default top-p 0.9, the flagged rate dropped from 25% to 14%, and the paired z change of -0.48 has an interval well clear of zero. The judge saw nothing: meaning 4.11 against 4.19, fluency 4.93 against 4.95, both differences inside noise. Temperature 1.0 was halfway and not clearly different from 0.7 on anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open the tail, and the text breaks before the watermark does.&lt;/strong&gt; Raising top-p to 1.0 at the same temperature took the flagged rate from 14% to 11%, which the intervals cannot separate. What it clearly did was damage the rewrites. Mean meaning fell by 0.37 points and fluency by 0.63, and a quarter of texts scored 3 or lower on meaning against 7% for the baseline. The judge's notes are specific: a science explainer dropped the inverse fourth-power law and invented "two to ten times"; a story dropped a mother's death date and reversed who said what in a dialogue; an incident report's "voltage drop below threshold" became "voltage reading below permissible level," which flips the sense. One in ten of these rewrites also read awkwardly, where none of the others did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The null does not move.&lt;/strong&gt; Clean rewrites scored a mean z of 0.07, -0.01, -0.04, 0.08 and 0.20 across the five settings, and they lost 5-grams at the same rate as the watermarked ones. The drop in signal is signal loss, not a shift in what clean text looks like to the detector.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing gets to zero.&lt;/strong&gt; Even the hottest setting left 11% of rewrites flagged at a 1% false-positive cutoff, and the AUC against the rewritten null was still 0.67. The point from the first post stands: too faint to accuse any one short text, strong enough to add up across a body of work.&lt;/p&gt;

&lt;p&gt;One more number. Across all 500 judged rewrites, the correlation between 5-grams kept and the meaning score was 0.15. Keeping the original's wording buys almost no faithfulness. Faithfulness comes from the model understanding the text, and copying is just a habit that happens to carry the watermark.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it means
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For someone trying to hide AI use:&lt;/strong&gt; the rewriter's temperature is a real lever, and it is free up to the nucleus cut. A hot rewrite with top-p left at its default removed about half of what the default setting left behind, at no measurable cost. Past the nucleus cut there is almost nothing more to gain, and the text starts losing facts. Deterministic rewriting is the worst choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For someone trying to detect AI use:&lt;/strong&gt; the attacker has a knob you cannot see, and the first half of its range costs them nothing. The trace that survives the best attacker setting here is about one rewrite in ten at a 1% false-positive rate. Your pooled statistics still work; your per-document verdicts get weaker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For the 5-gram proxy from the first two posts:&lt;/strong&gt; it held again. Every setting that lowered overlap lowered the signal, in order. If you cannot run a detector, counting surviving 5-grams still tells you most of the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One rewriter, one prompt, one seed per setting.&lt;/strong&gt; Temperature 0 is deterministic by construction. A different rewriter could have a different nucleus cut where the text breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One judge per item.&lt;/strong&gt; No second rater, no agreement check. The 1-to-5 scale is coarse, so read the paired intervals, not the raw means. The baseline is not spotless either; the judge caught the 0.7 run turning a tea price of "a crown per pound" into "one pound per crown," and two other settings turning a 17-year-old bistro into a 17-year-old owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100 texts per setting.&lt;/strong&gt; The flagged-rate intervals are wide. The AUC columns and the paired z differences are the stronger evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I did not vary top-k, or the generator's own temperature.&lt;/strong&gt; The watermark strength at generation time was fixed at the first post's setting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Ollama model is 4-bit quantized&lt;/strong&gt;, as before.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;Four short scripts on top of the first post's code: one runs the rewrites per setting through Ollama's HTTP API, one scores them with the same detector and counts surviving 5-grams, one does the paired analysis against the stored cutoffs, and one builds the blinded judge batches and collects the scores. About three hours of GPU time, nearly all of it rewriting.&lt;/p&gt;

&lt;p&gt;Thanks to the reader who asked. It was the right question, and the answer was not the one I would have guessed: the knob works, but the cliff is in top-p, not temperature.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>ollama</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Fancy prompts don't confuse small LLMs. One format line does</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/fancy-prompts-dont-confuse-small-llms-one-format-line-does-ih</link>
      <guid>https://dev.to/vadim_albarov/fancy-prompts-dont-confuse-small-llms-one-format-line-does-ih</guid>
      <description>&lt;p&gt;Ask an LLM to "improve" your prompt and you often get something like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kindly undertake a meticulous examination of the order correspondence reproduced hereunder and distill therefrom the following salient particulars...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Does a small local model still understand that? And the other way round: if you write in very simple words, short sentences, like for a child, does that help a 1B model?&lt;/p&gt;

&lt;p&gt;In my last test I broke prompts with typos and bad punctuation. This time the spelling is perfect. Only the &lt;em&gt;register&lt;/em&gt; changes: plain, normal, or ornate. I ran every version on eight local models from 0.6B to 14B, the biggest that fit in my 16 GB of VRAM.&lt;/p&gt;

&lt;p&gt;The short answer: the register itself barely matters. What matters is one line, the one that tells the model how to format its answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tested
&lt;/h2&gt;

&lt;p&gt;50 tasks, each with an answer a script can check. Five kinds, ten tasks each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Math&lt;/strong&gt;: multi-step word problems, answer is one number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract&lt;/strong&gt;: an order note, return JSON with five fixed keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify&lt;/strong&gt;: a support ticket, return one of four labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt;: a list of records, return the ones that match a condition, sorted. Each list has a trap at the edge: an employee aged exactly 30 for "older than 30", a flight at 14:05 for "between 10:00 and 14:00".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format&lt;/strong&gt;: writing tasks with strict rules. "Exactly 3 bullets, no word &lt;em&gt;health&lt;/em&gt;." "Write &lt;em&gt;yes&lt;/em&gt; 7 times."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each task exists in three versions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;plain&lt;/th&gt;
&lt;th&gt;normal&lt;/th&gt;
&lt;th&gt;ornate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;example&lt;/td&gt;
&lt;td&gt;Give only the number.&lt;/td&gt;
&lt;td&gt;Answer with the number only.&lt;/td&gt;
&lt;td&gt;Furnish solely the numerical figure, without accompanying exposition.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reading grade (Flesch-Kincaid)&lt;/td&gt;
&lt;td&gt;4.1&lt;/td&gt;
&lt;td&gt;8.3&lt;/td&gt;
&lt;td&gt;17.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;words per sentence&lt;/td&gt;
&lt;td&gt;9.8&lt;/td&gt;
&lt;td&gt;12.3&lt;/td&gt;
&lt;td&gt;22.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;words with 9+ letters&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The facts, numbers, and conditions are the same in all three versions. A Claude model checked every triple for differences in meaning before the run (it missed one, more on that below). Data you would paste, like the order note or the record list, is identical across versions. Only the words around it change.&lt;/p&gt;

&lt;p&gt;Models, all through Ollama with thinking off: Qwen3 0.6B, 1.7B, 4B-instruct, 8B, 14B and Gemma3 1B, 4B, 12B. Three trials per prompt with fixed seeds. 3,600 calls in the main run, plus 1,440 for a follow-up on math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 1: outside math, register makes no difference you can trust
&lt;/h2&gt;

&lt;p&gt;Here are the 40 non-math tasks. Percent correct, and the difference from normal with a 95% interval.&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;plain&lt;/th&gt;
&lt;th&gt;normal&lt;/th&gt;
&lt;th&gt;ornate&lt;/th&gt;
&lt;th&gt;plain − normal&lt;/th&gt;
&lt;th&gt;ornate − normal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qwen3 0.6B&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;−8 [−21, +3]&lt;/td&gt;
&lt;td&gt;+0 [−9, +9]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma3 1B&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;−9 [−19, +1]&lt;/td&gt;
&lt;td&gt;−8 [−19, +4]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3 1.7B&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;+1 [−8, +9]&lt;/td&gt;
&lt;td&gt;−8 [−17, +2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3 4B-instruct&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;td&gt;−8 [−18, +2]&lt;/td&gt;
&lt;td&gt;−10 [−22, +1]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma3 4B&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;−1 [−8, +7]&lt;/td&gt;
&lt;td&gt;−2 [−12, +8]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3 8B&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;−2 [−9, +6]&lt;/td&gt;
&lt;td&gt;−1 [−9, +7]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma3 12B&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;+4 [−4, +13]&lt;/td&gt;
&lt;td&gt;+3 [+0, +9]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3 14B&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;76&lt;/td&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;td&gt;+3 [−5, +10]&lt;/td&gt;
&lt;td&gt;+1 [−8, +9]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;From 8B up, the three versions land within 4 points of each other. Ornate English with 23-word sentences did not hurt them at all.&lt;/p&gt;

&lt;p&gt;At 4B and below there is a pattern: normal scores highest, and &lt;em&gt;both&lt;/em&gt; plain and ornate lose about 8 points. Every one of those intervals touches zero, so I can't call it proven. But it points one way. For a small model, very simple language is not a safe choice either. My plain prompts were slightly longer than the normal ones (more, shorter sentences), and the facts were spread over more sentences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 2: math swings 30 points, and it is not about reading
&lt;/h2&gt;

&lt;p&gt;Math looked like the big story. Pooled over all models: plain 28%, normal 34%, ornate 18%. qwen3 4B-instruct went from 93% on normal to 43% on ornate.&lt;/p&gt;

&lt;p&gt;Then I looked at the replies. When a model wrote out its working, it was right 79% of the time. When it gave a bare number, it was right 11% of the time. These models have thinking off, so writing the steps &lt;em&gt;is&lt;/em&gt; the thinking.&lt;/p&gt;

&lt;p&gt;And the format line decided how often they wrote the steps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;format line&lt;/th&gt;
&lt;th&gt;replies with working shown&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Answer with the number only." (normal)&lt;/td&gt;
&lt;td&gt;37%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Give only the number." (plain)&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Furnish solely the numerical figure, without accompanying exposition." (ornate)&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ornate prompts didn't confuse the models. The models obeyed them &lt;em&gt;more strictly&lt;/em&gt;, and that stopped them from reasoning.&lt;/p&gt;

&lt;p&gt;To separate the two effects, I kept each problem in its register but gave all three versions the same last line:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;setup&lt;/th&gt;
&lt;th&gt;plain&lt;/th&gt;
&lt;th&gt;normal&lt;/th&gt;
&lt;th&gt;ornate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;each register's own format line&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A: all end with "Answer with the number only."&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: all end with "Work it out step by step, then write the final number on the last line."&lt;/td&gt;
&lt;td&gt;84&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the same last line, the register of the problem text moves the score by 5 points at most. Changing the last line moves it by 45.&lt;/p&gt;

&lt;p&gt;The strangest part: two plain phrasings with the same meaning. qwen3 1.7B showed its working in 17% of replies to "Give only the number." and in 80% to "Answer with the number only." Same model, same problems, almost the same words.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 3: fancy words can change what you asked
&lt;/h2&gt;

&lt;p&gt;One extraction task had this note: &lt;em&gt;"Hi, this is Laura Becker from Becker Design. We'd like 12 desk lamps..."&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plain ("the name of the customer"): 88% answered Laura Becker.&lt;/li&gt;
&lt;li&gt;Normal ("customer name"): 96%.&lt;/li&gt;
&lt;li&gt;Ornate ("the appellation of the commissioning customer"): 25%. Most models answered &lt;strong&gt;Becker Design&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not really a wrong answer. A "commissioning customer" can well be the company. The ornate wording made the question vaguer, and the models picked the other reading. The Claude reviewer I used to check the triples for equal meaning did not flag it either. This is exactly the risk with prompts an LLM has rewritten: each fancy word is a small chance that the meaning moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it just what the model was trained on?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The theory.&lt;/strong&gt; Instruction-tuning data is mostly ordinary, direct English. So "normal" prompts should look the most familiar to a model, and both very simple and very ornate English should look less familiar. Small models cope worse with unfamiliar input. That would explain why small models did best on normal, and why big models did not care.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The test.&lt;/strong&gt; Perplexity measures how surprised a model is by a piece of text. Lower means more familiar. I scored the instruction wording of all 150 prompts with the same Qwen3 models (0.6B, 1.7B, 4B-instruct), leaving out the pasted data that is identical in all three versions.&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;plain&lt;/th&gt;
&lt;th&gt;normal&lt;/th&gt;
&lt;th&gt;ornate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qwen3 0.6B&lt;/td&gt;
&lt;td&gt;15.6&lt;/td&gt;
&lt;td&gt;16.7&lt;/td&gt;
&lt;td&gt;37.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3 1.7B&lt;/td&gt;
&lt;td&gt;15.9&lt;/td&gt;
&lt;td&gt;16.3&lt;/td&gt;
&lt;td&gt;31.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3 4B-instruct&lt;/td&gt;
&lt;td&gt;17.4&lt;/td&gt;
&lt;td&gt;17.4&lt;/td&gt;
&lt;td&gt;31.9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then I checked, task by task, whether the less familiar version was also the one the model got wrong more often. Outside math, the link was weak (r between −0.15 and +0.08) and not distinguishable from zero for any model. Inside math it was clear for the 4B (r = −0.41). But that is the format line again: the ornate "furnish solely the numerical figure" is both the most unfamiliar text and the line that stopped the model from reasoning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The explanation.&lt;/strong&gt; The theory is half right.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ornate English really does look foreign to these models: about twice the perplexity. But they understand it anyway. Unfamiliar words alone barely cost accuracy.&lt;/li&gt;
&lt;li&gt;Plain English looks exactly as familiar as normal. Yet small models still did a bit worse on it. So familiarity can't be the reason. The likely reason is structure: my plain versions split each task into more, shorter sentences, and a small model has to join the facts back together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So it is less about &lt;em&gt;which words&lt;/em&gt; you use and more about &lt;em&gt;how the task is laid out&lt;/em&gt;, and how strictly the model reads the format line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Don't bother rewriting a prompt into fancier English. It won't help, and it can shift the meaning.&lt;/li&gt;
&lt;li&gt;Don't dumb it down for small models either. Normal, direct English did as well as anything, and slightly better at 4B and below.&lt;/li&gt;
&lt;li&gt;Spend your effort on the format line. "Number only" on a model without thinking tells it to guess. If you want a correct number, let it show its work, and read the last line.&lt;/li&gt;
&lt;li&gt;If an LLM rewrote your prompt, read the rewritten version for words that could mean two things.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;50 tasks, three trials per prompt, fixed seeds, Ollama default temperatures. The registers were written by one person (with help from an LLM), so another writer's "ornate" could be harder or easier. The ornate prompts are longer than normal ones, so length and register are partly mixed. Only English. Only local models up to 14B with thinking off. Frontier models were not tested this time; my typos test suggests they would shrug all of this off. The perplexity check covers only the three smaller Qwen models, scored with full-precision weights, while the runs used Ollama's 4-bit builds. The "structure" explanation for plain is my best reading, not something this test isolated.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Repo with the prompts, the harness, and every raw reply: &lt;a href="https://github.com/valbarov/prompt-register-test" rel="noopener noreferrer"&gt;github.com/valbarov/prompt-register-test&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>promptengineering</category>
      <category>testing</category>
      <category>llm</category>
    </item>
    <item>
      <title>Your Laptop Speakers Pop When You Pause? It's Not a Bug, It's Power Saving</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Sat, 10 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/your-laptop-speakers-pop-when-you-pause-its-not-a-bug-its-power-saving-17j5</link>
      <guid>https://dev.to/vadim_albarov/your-laptop-speakers-pop-when-you-pause-its-not-a-bug-its-power-saving-17j5</guid>
      <description>&lt;p&gt;My ASUS Zephyrus has great speakers and one annoying habit: a &lt;strong&gt;pop&lt;/strong&gt; every time sound starts or stops. Pause a video: pop. Play again: pop. Click "skip 10 seconds" five times in a row on YouTube: pop-pop-pop-pop-pop.&lt;/p&gt;

&lt;p&gt;This is not an ASUS problem, and not a problem of one model. Many modern laptops have it. It seems most common on laptops with good speakers, where the speakers are driven by power-saving "smart amps". I have only tested one laptop, so take that part as a guess.&lt;/p&gt;

&lt;p&gt;This post shows how I found the cause and fixed it with a small PowerShell script. No admin rights, no driver hacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardware
&lt;/h2&gt;

&lt;p&gt;The speakers are not driven by a classic onboard codec. Device Manager shows a SoundWire setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cirrus Logic CS35L56&lt;/strong&gt; smart amps (left and right speakers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cirrus Logic CS42L43&lt;/strong&gt; codec (headphone jack, mics)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intel Smart Sound Technology&lt;/strong&gt; over SoundWire&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Smart amps like the CS35L56 save power. When the audio line goes silent, they shut down. When sound comes back, they power up again. On this laptop, that power change is loud enough to hear as a pop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Find out what causes the pop
&lt;/h2&gt;

&lt;p&gt;There are two common causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The stream&lt;/strong&gt; – the app opens or closes an audio stream, and the DSP mutes/unmutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The power-down&lt;/strong&gt; – the amp goes to sleep on silence and wakes up on sound.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To tell them apart, I wrote a test script (&lt;code&gt;Test-SilentKeeper.ps1&lt;/code&gt;) that plays a looping file of pure digital silence while I use the laptop as normal.&lt;/p&gt;

&lt;p&gt;The pop was &lt;strong&gt;still there&lt;/strong&gt;. That was a useful clue: pure zeros keep the Windows audio stream open, but the amp still detects silence and goes to sleep. So the amp itself watches the signal level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: What did not work
&lt;/h2&gt;

&lt;p&gt;Most forum posts say "disable power saving for the audio device". I tried that in the registry for the three SoundWire amp devices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;HKLM\SYSTEM\CurrentControlSet\Control\Class\{4d36e96c-e325-11ce-bfc1-08002be10318}\&amp;lt;instance&amp;gt;\PowerSettings&lt;/span&gt;
  &lt;span class="py"&gt;ConservationIdleTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;FF FF FF FF&lt;/span&gt;
  &lt;span class="py"&gt;PerformanceIdleTime&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;FF FF FF FF&lt;/span&gt;
  &lt;span class="py"&gt;IdlePowerState&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;00 00 00 00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a reboot: no change. The amp's silence gate runs inside the amp and its firmware, not through Windows device power management. I reverted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Keep the sound device busy... all the time
&lt;/h2&gt;

&lt;p&gt;The next idea: keep the sound device busy with a signal so quiet that you can't hear it, but loud enough that the amp doesn't see "silence".&lt;/p&gt;

&lt;p&gt;There are small tools that do exactly this. I used &lt;a href="https://github.com/vrubleg/soundkeeper" rel="noopener noreferrer"&gt;SoundKeeper&lt;/a&gt; (open source, MIT). Any similar tool that plays a steady, inaudible signal to the output device should work the same way. The important part is that it plays a real signal, not pure zeros, because the amp ignores zeros.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is not an advertisement. I have no ties to SoundKeeper or its author. It is simply the tool I found first, and it worked.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With SoundKeeper, the command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SoundKeeper64.exe sine f10 a5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a &lt;strong&gt;10 Hz sine wave at 5% volume&lt;/strong&gt;. Humans can't hear 10 Hz, and laptop speakers can't really play it anyway. But to the amp, it is "not silence", so it stays awake.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;no more pops.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The downside: the amps never sleep. That costs battery, and it feels wrong to keep the amps on all day just to avoid a pop when I pause a video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Keep the amp awake only when it matters
&lt;/h2&gt;

&lt;p&gt;What I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When I'm watching or listening to something, keep the amp awake, so pause, seek and skip don't pop.&lt;/li&gt;
&lt;li&gt;When nothing has played for a while, let the amp sleep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I wrote &lt;strong&gt;AmpPopGuard&lt;/strong&gt;: a watchdog that&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;reads the peak level of the default output device every 200 ms,&lt;/li&gt;
&lt;li&gt;starts the keeper tool (SoundKeeper in my case) when it hears real audio,&lt;/li&gt;
&lt;li&gt;stops it after &lt;strong&gt;10 seconds&lt;/strong&gt; with no real audio.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rapid seeking now happens inside the 10-second window, so the amp never sleeps between clicks. After real silence, you get one pop on the next cold start, and that's it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading the peak meter from PowerShell
&lt;/h3&gt;

&lt;p&gt;Windows exposes the output level through the Core Audio API (&lt;code&gt;IAudioMeterInformation&lt;/code&gt;). It's the same meter you see in the Volume Mixer. PowerShell can't call it directly, but &lt;code&gt;Add-Type&lt;/code&gt; with a few lines of C# COM interop works fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"C02216F6-8C67-4B5B-9D00-D008E73E0064"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
 &lt;span class="nf"&gt;InterfaceType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComInterfaceType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InterfaceIsIUnknown&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IAudioMeterInformation&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;GetPeakValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;pfPeak&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;Get the default render device from &lt;code&gt;IMMDeviceEnumerator&lt;/code&gt;, &lt;code&gt;Activate&lt;/code&gt; the meter, and call &lt;code&gt;GetPeakValue&lt;/code&gt; – you get a float from 0.0 to 1.0.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trap: the script hears itself
&lt;/h3&gt;

&lt;p&gt;The first version had a bug. The keeper's own 10 Hz sine shows up on the same peak meter at about &lt;strong&gt;0.01–0.02&lt;/strong&gt;. With a single threshold of 0.01, the watchdog started the keeper, then "heard audio" (its own sine), and never stopped it.&lt;/p&gt;

&lt;p&gt;The fix is two thresholds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Counts as real audio when peak &amp;gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keeper off&lt;/td&gt;
&lt;td&gt;0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keeper on&lt;/td&gt;
&lt;td&gt;0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;While the keeper runs, only sound clearly louder than its own sine resets the 10-second timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# AmpPopGuard.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Prevents the Cirrus amp pop on rapid pause/seek WITHOUT keeping the amps&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# awake all the time.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Only one instance at a time&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;mutex&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;System.Threading.Mutex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Global\AmpPopGuard'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;0&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="kr"&gt;exit&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="nv"&gt;$KeeperExe&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$PSScriptRoot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'SoundKeeper64.exe'&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# script lives next to the exe&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$KeeperArgs&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'sine'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'f10'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'a5'&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# 10 Hz sine @ 5% - inaudible, defeats the amp's silence gate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$LingerSeconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c"&gt;# keep amps awake this long after last real audio&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Two thresholds: the keeper's own 10 Hz sine reads ~0.01-0.02 on the meter,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# so while it runs, only levels above ThresholdOn count as real audio.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$ThresholdOff&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c"&gt;# start trigger while keeper is off&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$ThresholdOn&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c"&gt;# sustain trigger while keeper is running&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$PollMs&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$KeeperExe&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="n"&gt;Write-Error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SoundKeeper not found at &lt;/span&gt;&lt;span class="nv"&gt;$KeeperExe&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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="c"&gt;# --- Core Audio peak meter interop -----------------------------------------&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-Type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TypeDefinition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sh"&gt;@'
using System;
using System.Runtime.InteropServices;

[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
class MMDeviceEnumeratorComObject { }

[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator
{
    int NotImpl1();
    int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice ppDevice);
}

[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice
{
    int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams,
        [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
}

[Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioMeterInformation
{
    int GetPeakValue(out float pfPeak);
}

public static class AudioMeter
{
    static IAudioMeterInformation meter;

    public static float GetPeak()
    {
        if (meter == null)
        {
            var en = (IMMDeviceEnumerator)new MMDeviceEnumeratorComObject();
            IMMDevice dev;
            en.GetDefaultAudioEndpoint(0, 1, out dev);   // eRender, eMultimedia
            var iid = typeof(IAudioMeterInformation).GUID;
            object o;
            dev.Activate(ref iid, 23, IntPtr.Zero, out o); // CLSCTX_ALL
            meter = (IAudioMeterInformation)o;
        }
        float peak;
        meter.GetPeakValue(out peak);
        return peak;
    }

    public static void Reset() { meter = null; }  // call after device change errors
}
'@&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Stop-Keeper&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="n"&gt;Get-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'SoundKeeper64'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Stop-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&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="c"&gt;# Clean slate: no stray keeper from a previous run&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Stop-Keeper&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# --- Watchdog loop ----------------------------------------------------------&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$keeperOn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$lastLoud&lt;/span&gt;&lt;span class="w"&gt; &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="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;MinValue&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AmpPopGuard running (linger &lt;/span&gt;&lt;span class="nv"&gt;${LingerSeconds}&lt;/span&gt;&lt;span class="s2"&gt;s, thresholds &lt;/span&gt;&lt;span class="nv"&gt;$ThresholdOff&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$ThresholdOn&lt;/span&gt;&lt;span class="s2"&gt;). Keeper: &lt;/span&gt;&lt;span class="nv"&gt;$KeeperExe&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;try&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="kr"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$true&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="kr"&gt;try&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="nv"&gt;$peak&lt;/span&gt;&lt;span class="w"&gt; &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="n"&gt;AudioMeter&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;GetPeak&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="kr"&gt;catch&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="c"&gt;# default device changed / audio engine restarted - reconnect&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AudioMeter&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;Start-Sleep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Milliseconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1000&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="kr"&gt;continue&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="nv"&gt;$now&lt;/span&gt;&lt;span class="w"&gt; &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="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$threshold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$keeperOn&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="nv"&gt;$ThresholdOn&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="kr"&gt;else&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="nv"&gt;$ThresholdOff&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="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$peak&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$threshold&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="nv"&gt;$lastLoud&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$keeperOn&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="n"&gt;Start-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilePath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$KeeperExe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ArgumentList&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$KeeperArgs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-WindowStyle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Hidden&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nv"&gt;$keeperOn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&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="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$keeperOn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$lastLoud&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalSeconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LingerSeconds&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="n"&gt;Stop-Keeper&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nv"&gt;$keeperOn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$false&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="n"&gt;Start-Sleep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Milliseconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$PollMs&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="kr"&gt;finally&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="n"&gt;Stop-Keeper&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 &lt;code&gt;SoundKeeper64.exe&lt;/code&gt; in the same folder as the script. Get it from the &lt;a href="https://github.com/vrubleg/soundkeeper/releases" rel="noopener noreferrer"&gt;GitHub releases&lt;/a&gt; or the &lt;a href="https://veg.by/projects/soundkeeper/" rel="noopener noreferrer"&gt;project page&lt;/a&gt;. I use v1.3.6. To use a different keeper tool, change &lt;code&gt;$KeeperExe&lt;/code&gt;, &lt;code&gt;$KeeperArgs&lt;/code&gt; and the process name in &lt;code&gt;Stop-Keeper&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start it at login, hidden
&lt;/h2&gt;

&lt;p&gt;PowerShell's &lt;code&gt;-WindowStyle Hidden&lt;/code&gt; still flashes a window for a moment. A small VBS launcher in the Startup folder avoids that. Save as&lt;br&gt;
&lt;code&gt;%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\AmpPopGuard.vbs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vb"&gt;&lt;code&gt;&lt;span class="k"&gt;Set&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CreateObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WScript.Shell"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="s"&gt;C:\path\to\pwsh.exe&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="s"&gt; -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File &lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="s"&gt;C:\path\to\AmpPopGuard.ps1&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="s"&gt;"&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="k"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To stop it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name='pwsh.exe'"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nf"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CommandLine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AmpPopGuard.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Stop-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessId&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;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One pop is left&lt;/strong&gt;: the first sound after more than 10 s of silence still pops. That pop is part of how the amp works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: one COM call every 200 ms. CPU use is near zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuning&lt;/strong&gt;: &lt;code&gt;$LingerSeconds&lt;/code&gt; sets how long the amp stays awake. I use 10. The thresholds may need small changes on other hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device changes&lt;/strong&gt;: if you switch outputs (headphones, Bluetooth), the meter call fails, and the script reconnects to the new default device.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Does this apply to your laptop?
&lt;/h2&gt;

&lt;p&gt;Probably, if you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a pop or click when audio starts or stops,&lt;/li&gt;
&lt;li&gt;that gets worse when you seek or skip quickly,&lt;/li&gt;
&lt;li&gt;and "disable power saving" tweaks don't help.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many modern laptops from different brands use smart amps (Cirrus Logic CS35Lxx, TI TAS, Realtek and others). These amps often have the same silence gate. Higher-end laptops with better speakers seem to use them more, but I am not sure how wide it goes. The script does not depend on the brand or the model. It only watches the Windows peak meter.&lt;/p&gt;

&lt;p&gt;Have you seen this on other laptops? Tell me in the comments.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>tutorial</category>
      <category>showdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Your AWS Backup vault is locked. Who holds the key?</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Thu, 08 Oct 2026 16:59:12 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/your-aws-backup-vault-is-locked-who-holds-the-key-5eif</link>
      <guid>https://dev.to/vadim_albarov/your-aws-backup-vault-is-locked-who-holds-the-key-5eif</guid>
      <description>&lt;p&gt;AWS Backup Vault Lock in compliance mode is a strong guarantee. Once the cooling-off period ends, nobody can delete a recovery point early. Not an admin, not root, not AWS Support. For regulated workloads it is the closest thing to an offline tape you can get in the cloud.&lt;/p&gt;

&lt;p&gt;We recently moved our RDS backups to this setup: an AWS Backup plan, a local vault, and a copy of every recovery point to a locked vault in a second region. Then a reviewer asked one question that undid the whole feeling of safety.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What protects the KMS key that encrypts the locked vault?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nothing did. The vault key was a normal customer-managed key with the default policy. Any administrator in the account could disable it or schedule it for deletion. The recovery points would survive, locked and immutable, but nobody could decrypt them. A backup you cannot read is not a backup.&lt;/p&gt;

&lt;p&gt;This post is about what we tried, what failed in a way the docs would have told us, and what we ended up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Deny the destructive actions in the key policy
&lt;/h2&gt;

&lt;p&gt;Three KMS actions can make a key unusable: &lt;code&gt;kms:ScheduleKeyDeletion&lt;/code&gt;, &lt;code&gt;kms:DisableKey&lt;/code&gt;, and &lt;code&gt;kms:PutKeyPolicy&lt;/code&gt;. The third one matters because anyone who can rewrite the policy can remove the other two denies.&lt;/p&gt;

&lt;p&gt;A key policy can deny these to everyone except a single break-glass principal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DenyKeyDestruction"&lt;/span&gt;
  &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Deny"&lt;/span&gt;
  &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kms:ScheduleKeyDeletion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:DisableKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:PutKeyPolicy"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&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;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
    &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&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="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringNotEquals"&lt;/span&gt;
    &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:PrincipalArn"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;break_glass_role_arn&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;Two details are easy to miss here.&lt;/p&gt;

&lt;p&gt;First, this deny applies to the account root user too. The key policy is always evaluated, and an explicit deny in it beats the default root allow.&lt;/p&gt;

&lt;p&gt;Second, KMS will refuse to create this key. The lockout safety check rejects any policy that denies &lt;code&gt;PutKeyPolicy&lt;/code&gt; to the principal creating the key. You have to pass &lt;code&gt;bypass_policy_lockout_safety_check = true&lt;/code&gt;. That flag exists for exactly this reason: if the exempt principal turns out to be unusable, the key becomes permanently unmanageable. Remember that sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Who is the exempt principal?
&lt;/h2&gt;

&lt;p&gt;Our first exempt principal was the existing key-owners role in the same account. The reviewer rejected it in one line: any DevOps admin can assume that role. The deny adds one extra API call to an attack, not a wall.&lt;/p&gt;

&lt;p&gt;Inside a single account, this is a hard problem. An admin with &lt;code&gt;AdministratorAccess&lt;/code&gt; can edit any trust policy, attach any policy, and enrol a new MFA device on a stolen user. Whatever principal you exempt, the account can grant itself access to it.&lt;/p&gt;

&lt;p&gt;So we wanted the exempt principal outside the account. A break-glass role in a separate, tightly controlled account, assumable only with MFA by two named people. Prod credentials could never reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The test that saved us
&lt;/h2&gt;

&lt;p&gt;We did not apply this to the real key. We created a throwaway key with the same policy, plus an Allow statement granting the external break-glass role the key-management actions, and tried the role against it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws kms put-key-policy &lt;span class="nt"&gt;--key-id&lt;/span&gt; &amp;lt;test-key&amp;gt; &lt;span class="nt"&gt;--policy-name&lt;/span&gt; default &lt;span class="nt"&gt;--policy&lt;/span&gt; file://policy.json

An error occurred &lt;span class="o"&gt;(&lt;/span&gt;AccessDeniedException&lt;span class="o"&gt;)&lt;/span&gt; when calling the PutKeyPolicy operation:
This operation cannot be called cross account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same error for &lt;code&gt;DisableKey&lt;/code&gt;, &lt;code&gt;EnableKey&lt;/code&gt;, and &lt;code&gt;GetKeyPolicy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is in the KMS documentation and has been for years. A key policy can grant principals in other accounts permission to &lt;strong&gt;use&lt;/strong&gt; a key: encrypt, decrypt, generate data keys, describe it, create grants. It cannot let them &lt;strong&gt;administer&lt;/strong&gt; it. Key-management operations are refused cross-account, whatever the key policy says.&lt;/p&gt;

&lt;p&gt;Our throwaway key is now permanently unmanageable. Every principal in the account is denied by the key policy. The only exempt principal is in another account and cannot call the API. AWS Support is the documented way to recover such a key. It costs a dollar a month and encrypts nothing, so we left it as a monument.&lt;/p&gt;

&lt;p&gt;Picture that on the real vault key, holding every copy of a production database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The design that works
&lt;/h2&gt;

&lt;p&gt;The exempt principal must live in the same account as the key. The question becomes: can we make a same-account role that no same-account identity can assume?&lt;/p&gt;

&lt;p&gt;Yes. Trust policies decide who can assume a role, and IAM permissions cannot override a trust policy. A role whose trust policy names only a principal from another account is unreachable from inside, no matter how many admin policies the caller holds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"dr_key_break_glass"&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="s2"&gt;"${var.env}-dr-key-break-glass"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;external_break_glass_role_arn&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="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"dr_key_break_glass"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dr_key_break_glass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"kms:DescribeKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:GetKeyPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:PutKeyPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"kms:DisableKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:EnableKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"kms:ScheduleKeyDeletion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"kms:CancelKeyDeletion"&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_kms_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dr_vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key policy deny exempts this role's ARN. The creating principal is still denied &lt;code&gt;PutKeyPolicy&lt;/code&gt;, so the bypass flag stays. The role is not listed as a principal anywhere in the key policy; the default root statement lets IAM policies take effect, and the inline policy above does the rest. That also avoids a race on first apply, where KMS rejects a key policy naming an IAM principal that was created seconds ago.&lt;/p&gt;

&lt;p&gt;The legitimate path is two hops. A named user assumes the external break-glass role with MFA. That session assumes the in-account role. That session administers the key. Role chaining drops the MFA context, so the MFA condition lives on the first hop only.&lt;/p&gt;

&lt;p&gt;We tested again on a second throwaway key:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Caller&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Account admin&lt;/td&gt;
&lt;td&gt;AssumeRole into the in-account role&lt;/td&gt;
&lt;td&gt;AccessDenied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Account admin&lt;/td&gt;
&lt;td&gt;DisableKey, ScheduleKeyDeletion, PutKeyPolicy&lt;/td&gt;
&lt;td&gt;Explicit deny&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-hop chain&lt;/td&gt;
&lt;td&gt;GetKeyPolicy, PutKeyPolicy&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-hop chain&lt;/td&gt;
&lt;td&gt;DisableKey, then EnableKey&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-hop chain&lt;/td&gt;
&lt;td&gt;ScheduleKeyDeletion&lt;/td&gt;
&lt;td&gt;OK, used for cleanup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What an attacker has left
&lt;/h2&gt;

&lt;p&gt;One path. An admin in the account can edit the in-account role's trust policy to name themselves. That is an IAM write, &lt;code&gt;iam:UpdateAssumeRolePolicy&lt;/code&gt; on a specific role name. IAM management events land in CloudTrail by default, so an EventBridge rule can page someone the moment it happens. Compare that with the original design, where the attack was a plain &lt;code&gt;AssumeRole&lt;/code&gt; on a role the team uses every day.&lt;/p&gt;

&lt;p&gt;To close that last path you need a control the account cannot undo. A Service Control Policy on the organizational unit, denying those IAM writes on the role and the three KMS actions on the key to every principal except the break-glass role, does it. An SCP mistake is reversible from the management account. A key policy mistake is not. A dedicated backup account that owns the vault and the key is the stronger end state. Both need the org management account, which for us meant a ticket to a partner. The in-account role shipped in the meantime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational costs, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Any Terraform apply that changes the key policy must run through the two-hop chain. Adding a new consumer does not, because the local admin role can still issue grants.&lt;/li&gt;
&lt;li&gt;Destroying the module needs the chain too.&lt;/li&gt;
&lt;li&gt;If the external break-glass role is deleted and recreated, the trust policy keeps the old principal ID. The next apply fixes it, but the chain is dead until then.&lt;/li&gt;
&lt;li&gt;The deny matches the role ARN as a string. Recreating the in-account role with the same name keeps working. Renaming it freezes the policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Three habits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Read the current docs before designing an IAM control.&lt;/strong&gt; The design came from Claude Code, which answered from memory. The cross-account rule was one paragraph away, on a page neither of us read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test anything that can lock a resource on something disposable.&lt;/strong&gt; The lockout safety check is a warning, not an obstacle. If you bypass it on purpose, prove that the principal you trust can actually act, before the real key exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask what protects the thing that protects the thing.&lt;/strong&gt; A locked vault, a versioned bucket, an immutable snapshot. Each one depends on a key, a policy, or a role that someone can still change. Follow the chain until you reach something the account itself cannot undo, and if you cannot reach it, know exactly which step is left open.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>devops</category>
      <category>backup</category>
    </item>
    <item>
      <title>I Watermarked 100 Texts With SynthID and Asked Local LLMs to Wash It Out. The Detector Still Saw Some of It.</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Tue, 06 Oct 2026 15:00:25 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/i-watermarked-100-texts-with-synthid-and-asked-local-llms-to-wash-it-out-the-detector-still-saw-500i</link>
      <guid>https://dev.to/vadim_albarov/i-watermarked-100-texts-with-synthid-and-asked-local-llms-to-wash-it-out-the-detector-still-saw-500i</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/vadim_albarov/can-a-local-llm-wash-out-a-watermark-without-washing-out-the-meaning-i-tested-300-rewrites-15jb"&gt;last post&lt;/a&gt; I asked whether a local LLM can rewrite an AI text enough to break its word patterns while keeping the meaning. The answer was yes, with a good 12–14B model. But I only &lt;em&gt;estimated&lt;/em&gt; watermark removal by counting how many word sequences survived. I had no real watermark and no real detector.&lt;/p&gt;

&lt;p&gt;This time I had both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The question:&lt;/strong&gt; if a text carries Google's SynthID-Text watermark, and you run it through a local model with "rewrite this in your own words," can a detector still tell?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; mostly no, but not entirely. The standard detector flagged 3% to 15% of the rewrites, depending on the rewriting model. A simpler, more sensitive test flagged 23% to 41%. For long texts rewritten by the most faithful model, it flagged 89%. The model that kept the meaning best in the last test also kept the most watermark. And rewriting a second time did not help at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SynthID-Text does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/blog/synthid-text" rel="noopener noreferrer"&gt;SynthID-Text&lt;/a&gt; is Google DeepMind's text watermark. It ships in Hugging Face &lt;code&gt;transformers&lt;/code&gt;. It changes nothing about the model weights. While the model writes, a logits processor looks at the last 4 tokens, uses them plus a secret key to split the vocabulary into "preferred" and "not preferred" tokens, and nudges the sampling toward the preferred ones. The text still reads normally.&lt;/p&gt;

&lt;p&gt;A detector needs only the key and the tokenizer, not the model. For each token it asks: given the 4 tokens before it, was this a preferred token? In normal text that is true about half the time. In watermarked text it is true more often. SynthID calls this score the &lt;em&gt;g-value&lt;/em&gt;. Human text has a mean g-value of 0.500. Watermarked text sits above it.&lt;/p&gt;

&lt;p&gt;Two ways to turn g-values into a verdict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Bayesian detector.&lt;/strong&gt; This is the one in the Hugging Face example. It is a tiny model (about 900 parameters) that you train on watermarked and clean text. It outputs a probability that the text is watermarked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The mean test.&lt;/strong&gt; Just check whether the mean g-value is above 0.5 by more than chance allows. I report it as a z-score. Under no watermark the z-score is about 0; a z of 2.3 or more happens in about 1% of clean texts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Everything ran on one laptop GPU with 16 GB of VRAM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A watermarked writer.&lt;/strong&gt; Gemma is gated on Hugging Face and I was not logged in, so I used &lt;code&gt;Qwen/Qwen3-4B-Instruct-2507&lt;/code&gt; in bf16 with the SynthID config from the Hugging Face example: 30 keys, n-gram length 5, temperature 1.0, top-k 40. Everything you need is &lt;code&gt;watermarking_config=SynthIDTextWatermarkingConfig(...)&lt;/code&gt; passed to &lt;code&gt;model.generate()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A detector.&lt;/strong&gt; I followed the training recipe from &lt;code&gt;transformers/examples/research_projects/synthid_text&lt;/code&gt;, minus TensorFlow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Watermarked examples: 1,200 answers to "explain like I'm five" questions from the ELI5 dataset, about 290 tokens each.&lt;/li&gt;
&lt;li&gt;Clean examples: 3,000 English Wikipedia articles and 480 answers to other ELI5 questions written by the same model &lt;em&gt;without&lt;/em&gt; the watermark.&lt;/li&gt;
&lt;li&gt;70/30 train/validation split, 100 epochs, 6 minutes of training.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the validation set, the detector caught &lt;strong&gt;99.7%&lt;/strong&gt; of watermarked 200-token snippets at a 1% false-positive rate, and 98.1% at 0.1%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The test texts.&lt;/strong&gt; The same 100 topics as last time: 10 genres (news, technical explainers, science, opinion, personal stories, marketing, how-to, business memos, history, short fiction), 190 to 900 words each. The watermarked model wrote all 100. It also wrote the same 100 &lt;em&gt;without&lt;/em&gt; the watermark, as a control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The rewrites.&lt;/strong&gt; The same three Ollama models and the same prompt as last time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;qwen3:4b-instruct&lt;/code&gt; (14 minutes for 100 texts)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gemma3:12b&lt;/code&gt; (27 minutes)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;qwen3:14b&lt;/code&gt;, thinking off (50 minutes, including a slow patch; the median was 15 seconds per text)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the first round I also ran a &lt;strong&gt;second pass&lt;/strong&gt;: Gemma rewrote its own 100 rewrites once more, same prompt (37 minutes).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rewrite the text below completely in your own words.
- Change the vocabulary and the sentence structure; do not copy phrases.
- Keep ALL of the meaning: every fact, name, number, date, step, argument, and plot event.
- Do not add new information, comments, or opinions.
- Keep the same genre, tone, and roughly the same length. Keep the title line, rewritten.
Output only the rewritten text, nothing else.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. The scoring.&lt;/strong&gt; I tokenized every text with the Qwen tokenizer and ran both detectors. The "flagged" cutoffs were set so that 1% of 500 clean control texts get flagged. The controls were the 100 unwatermarked Qwen texts, 300 held-out Wikipedia articles, and the 100 Claude-written texts from my last post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the watermark work at all?
&lt;/h2&gt;

&lt;p&gt;Yes, very well. This is the baseline the rewrites have to beat.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Text&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;Flagged (Bayesian)&lt;/th&gt;
&lt;th&gt;Flagged (mean test)&lt;/th&gt;
&lt;th&gt;Mean g-value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Watermarked originals&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.546&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same prompts, no watermark&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;td&gt;0.500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wikipedia (human)&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;0.500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude-written texts (other AI)&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0.500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every watermarked text scored a probability of 1.0 or close to it. The weakest original, a 330-token piece, still came in at 0.99994. The watermark also shows up fast: using only the first 100 tokens of each original, 94% were flagged; at 200 tokens, 100%.&lt;/p&gt;

&lt;p&gt;The control numbers are the point of reference below. "1%" means "no better than chance."&lt;/p&gt;

&lt;h2&gt;
  
  
  Does a rewrite remove it?
&lt;/h2&gt;

&lt;p&gt;Mostly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rewriter&lt;/th&gt;
&lt;th&gt;Flagged (Bayesian)&lt;/th&gt;
&lt;th&gt;Flagged (mean test)&lt;/th&gt;
&lt;th&gt;Mean g-value&lt;/th&gt;
&lt;th&gt;Mean z&lt;/th&gt;
&lt;th&gt;AUC (mean test)&lt;/th&gt;
&lt;th&gt;5-grams kept from original&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gemma3:12b&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;td&gt;0.5054&lt;/td&gt;
&lt;td&gt;1.56&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:4b-instruct&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.5056&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:14b&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;41%&lt;/td&gt;
&lt;td&gt;0.5087&lt;/td&gt;
&lt;td&gt;2.52&lt;/td&gt;
&lt;td&gt;0.87&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma3:12b, second pass&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;0.5063&lt;/td&gt;
&lt;td&gt;1.79&lt;/td&gt;
&lt;td&gt;0.83&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four things to read from this table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The standard detector lost most of the trail.&lt;/strong&gt; The Bayesian detector, which is the one you would deploy, flagged 3% of Gemma's rewrites. That is close to the 1% you would get on clean text. Across all 300 rewrites, only 18 of the 100 originals had even one rewrite flagged by it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But the watermark did not fully disappear.&lt;/strong&gt; Every rewriter's mean g-value sits above the clean baseline of 0.500. The difference is small for any single text, which is why the Bayesian detector rarely fires. Pooled over 100 texts it is unmistakable: a z of 17 for Gemma, 16 for the 4B model, and 28 for the 14B model. Clean controls pooled the same way give 0.2 to 1.2. The simple mean test, which is more sensitive to weak signals than the trained detector in this setup, flagged 23% to 41% of individual rewrites, and 53 of the 100 originals had at least one rewrite it caught.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best meaning-keeper is the worst watermark-washer.&lt;/strong&gt; Last time, &lt;code&gt;qwen3:14b&lt;/code&gt; kept the meaning best (4.68 out of 5) but also kept the most original wording. Here it also kept the most watermark: 41% of its rewrites were flagged by the mean test, 15% by the Bayesian detector, and 8% of them scored above 0.95, which the Hugging Face example treats as "watermarked." Gemma, the heaviest rewriter, washed out the most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A second rewrite did not remove more. It removed less.&lt;/strong&gt; I expected that rewriting the rewrite would halve the trace again. It went the other way. Gemma's second pass scored a little &lt;em&gt;higher&lt;/em&gt; than its first: mean z 1.79 versus 1.56, flagged by the mean test 38% versus 26%, AUC 0.83 versus 0.79. The second pass was a real rewrite; it shared only 24% of its 5-grams with the first pass. But in 86 of 100 texts it shared &lt;em&gt;more&lt;/em&gt; 5-grams with the watermarked original than the first pass did (14% versus 10%). When a model paraphrases a paraphrase, it tends to fall back on the most natural phrasing, and the most natural phrasing is often what the original model picked too. The watermark lives in exact token sequences, so landing back on the original's phrases means landing back on its watermark. The per-text increase is small (paired t of 1.8, so not clearly significant), but there is no sign of a decrease.&lt;/p&gt;

&lt;h3&gt;
  
  
  Longer texts are easier to catch
&lt;/h3&gt;

&lt;p&gt;A watermark detector adds up evidence token by token. A rewrite leaves a little evidence per token, so the longer the text, the more it adds up.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Original length&lt;/th&gt;
&lt;th&gt;Rewrites&lt;/th&gt;
&lt;th&gt;Flagged (mean test)&lt;/th&gt;
&lt;th&gt;Flagged (Bayesian)&lt;/th&gt;
&lt;th&gt;Mean z&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;150–250 words&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300–450 words&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;29%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;1.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500–700 words&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;45%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;750–900 words&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;70%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;3.9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By model, for the 750–900 word texts: &lt;code&gt;qwen3:14b&lt;/code&gt; rewrites were flagged 89% of the time, &lt;code&gt;gemma3:12b&lt;/code&gt; 78%, &lt;code&gt;qwen3:4b-instruct&lt;/code&gt; 44%. For 150–250 word texts, all three were near the noise floor (3% to 15%).&lt;/p&gt;

&lt;h3&gt;
  
  
  Fiction is the hardest to wash
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Genre&lt;/th&gt;
&lt;th&gt;Flagged (mean test)&lt;/th&gt;
&lt;th&gt;Flagged (Bayesian)&lt;/th&gt;
&lt;th&gt;5-grams kept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Short fiction&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;70%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;47%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal narrative&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business documents&lt;/td&gt;
&lt;td&gt;37%&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How-to guides&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketing copy&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Science&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opinion essays&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technical explainers&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;News&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The models rewrote stories much more timidly than they rewrote explainers. They kept names, dialogue, and specific images, and with them the token sequences that carry the watermark. All three top-scoring rewrites in the whole test were &lt;code&gt;qwen3:14b&lt;/code&gt; stories, each with a probability of 1.0.&lt;/p&gt;

&lt;p&gt;Here is the opening of one, a time-travel story. The original:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It was 6:47 a.m. when Elias Varell stepped onto the cracked asphalt of Maple Street, his boots clicking against the frost-bitten sidewalk. The bus stop, a tattered wooden bench leaning at an angle like some tired old tooth, sat beneath a flickering traffic light that had failed to turn green for the last forty-one minutes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;qwen3:14b&lt;/code&gt; rewrite:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It was 6:47 a.m. when Elias Varell arrived at the broken pavement of Maple Street, his boots making a tapping sound on the frozen path. The bus stop, a worn wooden bench leaning at an odd angle, stood under a flickering traffic light that had remained red for forty-one minutes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It reads as a rewrite. But 51% of the original's five-word sequences survived, the longest copied run was 66 words, and the detector gave it a probability of 1.0. Gemma's rewrite of the same story got a probability of 0.0, with a mean g-value of 0.509 and a z of 3.3. Still above the noise, but a deployed detector would have let it through.&lt;/p&gt;

&lt;h3&gt;
  
  
  The n-gram proxy from last time was fair
&lt;/h3&gt;

&lt;p&gt;Last time I used "share of the original's 5-grams that survive" as a stand-in for watermark removal. Now I can check it against the real thing. Across the 300 first-pass rewrites, the correlation between 5-grams kept and the watermark z-score was &lt;strong&gt;0.77&lt;/strong&gt; (0.73 with the second pass included).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;5-grams kept&lt;/th&gt;
&lt;th&gt;Rewrites&lt;/th&gt;
&lt;th&gt;Flagged (mean test)&lt;/th&gt;
&lt;th&gt;Mean z&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;under 10%&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10–20%&lt;/td&gt;
&lt;td&gt;112&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;1.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20–30%&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;td&gt;3.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;over 30%&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a rewrite keeps less than a tenth of the original's five-word sequences, it is nearly clean. If it keeps more than a fifth, the watermark is probably still there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For someone trying to hide AI use:&lt;/strong&gt; one pass through a local 12B model removes the watermark well enough to beat the standard detector most of the time, especially on short, factual text. Long texts and stories are the exception. Rewriting twice does not close that gap; in this test it widened it slightly. What does reduce the trace is a rewriter that keeps less of the original wording, and shorter text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For someone trying to detect AI use:&lt;/strong&gt; the watermark is extremely robust to nothing-done and weak to paraphrase, which matches what the SynthID paper says. But "weak" is not "gone." A trace survives in nearly every rewrite. It is too faint to accuse any one short document, but strong enough to show up in a long document or across a body of work. If your detector sees 50 essays from one source, the pooled statistic will tell you something even when no single essay does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For the "which local model" question:&lt;/strong&gt; the trade-off from last post is now sharper. &lt;code&gt;qwen3:14b&lt;/code&gt; keeps the meaning and keeps the watermark. &lt;code&gt;gemma3:12b&lt;/code&gt; loses a little precision and loses the watermark. There is no free lunch in this set of three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One generator, one setting.&lt;/strong&gt; Watermark strength depends on how much freedom the sampler has. Temperature 1.0 with top-k 40 is friendly to the watermark. A lower temperature, or a more deterministic task like code, would start weaker and wash out faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My detector is smaller than Google's recipe.&lt;/strong&gt; The example recommends 10,000 examples per class; I used about 1,200 watermarked and 3,500 clean. Validation accuracy was 99.7%, so it is not underfit, but a bigger detector might be better calibrated on partial watermarks. The Bayesian detector's outputs were very polarized (near 0 or near 1), which is why the plain mean test caught more borderline rewrites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 1% false-positive cutoffs come from 500 clean texts.&lt;/strong&gt; That is enough for a rough cutoff, not a precise one. The AUC numbers do not depend on a cutoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One seed, one prompt.&lt;/strong&gt; Same as last time. The second pass was tested with one model only (Gemma on its own output). A second pass with a &lt;em&gt;different&lt;/em&gt; model might behave differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Ollama models are 4-bit quantized.&lt;/strong&gt; Full-precision versions might rewrite slightly differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The detector needs the generator's tokenizer and key.&lt;/strong&gt; That is how SynthID works in practice: the model owner runs the detector. A third party without the key cannot do any of this.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;Everything is a few short Python scripts on top of &lt;code&gt;transformers&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate watermarked and clean training text with &lt;code&gt;model.generate(..., watermarking_config=...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Compute g-values with &lt;code&gt;SynthIDTextWatermarkLogitsProcessor.compute_g_values()&lt;/code&gt; and train a &lt;code&gt;BayesianDetectorModel&lt;/code&gt; on them.&lt;/li&gt;
&lt;li&gt;Generate the 100 test texts with the watermark, and 100 without.&lt;/li&gt;
&lt;li&gt;Rewrite with Ollama over the local HTTP API (and once more for the second pass).&lt;/li&gt;
&lt;li&gt;Tokenize everything with the generator's tokenizer, score with both detectors, and compare to the clean controls.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total GPU time was a little over four hours, most of it generation and rewriting. Detection itself takes seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Update: held-out cutoffs, a rewritten null, and genre at matched length
&lt;/h2&gt;

&lt;p&gt;Readers pointed out three holes in the method above. All three were real. I fixed them and re-ran the numbers. The outcome did not change: a trace of the watermark survives every rewriter, and the pooled statistics are untouched. The per-text percentages moved by a few points, and each one now comes with an interval. Here is what was wrong and what the fixes showed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hole 1: the 1% cutoff was set and measured on the same texts
&lt;/h3&gt;

&lt;p&gt;The cutoff was the 99th percentile of 500 clean texts, and the control table reported how those same 500 texts fell against it. That 1% was true by construction. With 5 texts above the line, the cutoff itself was shaky.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; I pulled 1,200 more held-out Wikipedia articles, bringing the clean set to 1,700 texts, and split every clean set in half by text id. One half (850 texts) sets the cutoffs. The other half (850 texts) measures the false-positive rate. The calibrated z cutoff came out at 2.42; theory for a 1% rate says 2.33.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Held-out clean text&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;False positives (mean test)&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;th&gt;False positives (Bayesian)&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;850&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;td&gt;0.6 to 2.1&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;0.7 to 2.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same-domain subset&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;1.6%&lt;/td&gt;
&lt;td&gt;0.6 to 4.0&lt;/td&gt;
&lt;td&gt;0.8%&lt;/td&gt;
&lt;td&gt;0.2 to 2.9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The operating point transfers. The rewrite table at the new cutoffs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rewriter&lt;/th&gt;
&lt;th&gt;Flagged (Bayesian)&lt;/th&gt;
&lt;th&gt;Flagged (mean test)&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;th&gt;Was&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gemma3:12b&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;18 to 34&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:4b-instruct&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;td&gt;11 to 26&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:14b&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;39%&lt;/td&gt;
&lt;td&gt;30 to 49&lt;/td&gt;
&lt;td&gt;41%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma3:12b, second pass&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;td&gt;36%&lt;/td&gt;
&lt;td&gt;27 to 46&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Originals with at least one rewrite caught by the mean test: 57 of 100 (was 53). The pooled z values and the AUCs do not depend on a cutoff and are unchanged. The honest reading is that every per-text percentage in this post is soft to about plus or minus 8 points. The ordering of the rewriters, and the gap between any of them and clean text, is not in doubt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hole 2: the clean controls were never rewritten
&lt;/h3&gt;

&lt;p&gt;I compared rewritten watermarked text against clean text that had not been rewritten. If a rewriter nudges clean scores up on its own, some of the "residual watermark" would have been rewriter artifact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; I ran the 100 unwatermarked texts, which come from the same 100 prompts, through the identical pipeline: same three rewriters, same prompt, same temperature, same seed. The rewriters changed clean and watermarked text to the same degree (5-grams kept: 10% versus 10%, 10% versus 11%, 19% versus 18%).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rewriter&lt;/th&gt;
&lt;th&gt;Clean rewrites, mean z&lt;/th&gt;
&lt;th&gt;Watermarked rewrites, mean z&lt;/th&gt;
&lt;th&gt;Clean rewrites flagged&lt;/th&gt;
&lt;th&gt;Watermarked rewrites flagged&lt;/th&gt;
&lt;th&gt;AUC vs clean rewrites&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none (originals)&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;12.13&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma3:12b&lt;/td&gt;
&lt;td&gt;-0.01&lt;/td&gt;
&lt;td&gt;1.56&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:4b-instruct&lt;/td&gt;
&lt;td&gt;0.12&lt;/td&gt;
&lt;td&gt;1.50&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;td&gt;0.78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:14b&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;td&gt;2.52&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;39%&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rewriting does not move the clean distribution. The shift in mean z is at most 0.12 and a Mann-Whitney test against the unrewritten clean texts gives p of 0.40 or higher for all three. Not one of the 300 clean rewrites crossed the cutoff. The AUC against the rewritten null is the same as against the unrewritten null. Paired by prompt, the watermarked rewrite scored above its clean twin in 80% to 89% of pairs (paired t 7.5 to 10.0). Inside each length bucket the clean rewrites are flagged 0% of the time for every rewriter, while the watermarked ones climb with length as before. The residual is watermark, not rewriter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hole 3: the genre table mixed in a length effect
&lt;/h3&gt;

&lt;p&gt;My fiction texts averaged 745 tokens, my technical explainers 513, and detection rises with length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; A least-squares fit of z on log2(tokens) plus genre over the 300 first-pass rewrites gives 1.64 z per doubling of length. Fiction's raw advantage over technical explainers is 3.22 z. After adjusting for length it is 2.38 z, with a 95% interval of 1.3 to 3.6 (10 fiction sources and 10 explainer sources, 3 rewrites each). About a quarter of the fiction effect was length. Three quarters is genre.&lt;/p&gt;

&lt;p&gt;The interval comes from a bootstrap that resamples the 100 original texts, not the 300 rewrites. Each original has three rewrites, one per rewriter, and they share its topic, length, and watermark strength. Treating them as independent gave a standard error of 0.39 in the first version of this update. Grouped by source it is 0.57 (10,000 resamples; an earlier run with fewer resamples gave 0.58). The effect is still well clear of zero. (Edited October 9 after a reader pointed this out.)&lt;/p&gt;

&lt;p&gt;Dropping any one of the 100 source texts and refitting leaves fiction first in all 100 refits, with a difference of 2.1 to 2.7 z. (Added October 18.)&lt;/p&gt;

&lt;p&gt;Inside one bucket, where lengths match:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Fiction: tokens, mean z, flagged&lt;/th&gt;
&lt;th&gt;Technical explainer: tokens, mean z, flagged&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;150–250 words&lt;/td&gt;
&lt;td&gt;336, 1.1, 0% (n=6)&lt;/td&gt;
&lt;td&gt;334, 0.3, 0% (n=15)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;300–450 words&lt;/td&gt;
&lt;td&gt;632, 3.4, 67% (n=12)&lt;/td&gt;
&lt;td&gt;610, 1.9, 33% (n=9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500–700 words&lt;/td&gt;
&lt;td&gt;937, 6.8, 100% (n=6)&lt;/td&gt;
&lt;td&gt;812, 2.2, 33% (n=6)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Personal narrative is the only other genre with a clear length-adjusted effect (+1.3 z, interval 0.6 to 2.0, 10 sources). Every other genre sits within 0.7 z of explainers once length is accounted for, and none of their intervals excludes zero. All genres, length-adjusted, compared with technical explainers (10 sources × 3 rewrites per genre, 95% intervals from the grouped bootstrap):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Genre&lt;/th&gt;
&lt;th&gt;Sources&lt;/th&gt;
&lt;th&gt;Rewrites&lt;/th&gt;
&lt;th&gt;Difference vs explainer (z)&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;fiction (short story)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;+2.38&lt;/td&gt;
&lt;td&gt;1.30 to 3.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;personal narrative&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;+1.30&lt;/td&gt;
&lt;td&gt;0.59 to 2.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;product description / marketing&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;+0.29&lt;/td&gt;
&lt;td&gt;-0.49 to 1.09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;business / professional&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;+0.13&lt;/td&gt;
&lt;td&gt;-0.74 to 0.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;how-to guide&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;+0.01&lt;/td&gt;
&lt;td&gt;-0.70 to 0.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;history / culture&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;-0.14&lt;/td&gt;
&lt;td&gt;-0.86 to 0.52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;news report&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;-0.30&lt;/td&gt;
&lt;td&gt;-1.06 to 0.42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;science popularization&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;-0.46&lt;/td&gt;
&lt;td&gt;-1.28 to 0.37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opinion essay&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;-0.68&lt;/td&gt;
&lt;td&gt;-1.38 to 0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are differences from explainers, not tests of whether the watermark survived in a genre. An interval that includes zero means that, with 10 sources, I cannot tell that genre apart from explainers. It does not mean no watermark survived there. The survival claim in this post is per rewriter, pooled over all genres. The genre result is narrower: fiction and personal narrative keep more of the watermark than explainers at the same length. (Edited October 12 after the same reader asked for source counts next to the intervals.)&lt;/p&gt;

&lt;p&gt;The clean rewrites show no genre pattern at all (fiction clean rewrites: mean z 0.20), so this is in the watermark, not in how the rewriters treat stories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sensitivity check: leave one source out
&lt;/h3&gt;

&lt;p&gt;The same reader asked whether the fiction-versus-explainer ordering is shared across the fiction texts or carried by a few of them. A genre mean from 10 sources can move a lot if one strong source is removed. So I refit the same length-adjusted model 100 times, each time dropping one of the 100 original texts together with its three rewrites.&lt;/p&gt;

&lt;p&gt;Fiction stays on top in all 100 refits. Its length-adjusted difference from explainers ranges from 2.14 to 2.70 z (full sample 2.38). The low end comes from dropping t093, the story with the strongest surviving trace (mean z 7.3 over its three rewrites). The high end comes from dropping t095, the story with the weakest (mean z 0.3). Even the lowest value sits above the lower bound of the grouped bootstrap interval (1.30). Personal narrative stays second in all 100 refits, between 1.12 and 1.47 z.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fiction source dropped&lt;/th&gt;
&lt;th&gt;Fiction difference vs explainer (z)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none (full sample)&lt;/td&gt;
&lt;td&gt;+2.38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t091&lt;/td&gt;
&lt;td&gt;+2.49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t092&lt;/td&gt;
&lt;td&gt;+2.48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t093&lt;/td&gt;
&lt;td&gt;+2.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t094&lt;/td&gt;
&lt;td&gt;+2.36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t095&lt;/td&gt;
&lt;td&gt;+2.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t096&lt;/td&gt;
&lt;td&gt;+2.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t097&lt;/td&gt;
&lt;td&gt;+2.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t098&lt;/td&gt;
&lt;td&gt;+2.48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t099&lt;/td&gt;
&lt;td&gt;+2.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t100&lt;/td&gt;
&lt;td&gt;+2.26&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Dropping any one explainer source moves the fiction difference between 2.27 and 2.56. The middle genres are a different story. The how-to guide estimate changes sign in 12 of the 100 refits, and business and history each flip once. Those three sit within 0.15 z of explainers, so a sign flip there means nothing. It just restates that their intervals include zero.&lt;/p&gt;

&lt;p&gt;This is a sensitivity check on the texts I have, not more evidence about fiction in general. With 10 fiction sources the population claim stays as wide as the bootstrap interval says. What the check adds is that the ordering inside this sample does not hinge on one or two stories. The pooled survival result per rewriter is not part of this check and does not change. (Added October 18 after the same reader asked for a leave-one-source-out view.)&lt;/p&gt;

&lt;p&gt;UPD. Oct-21&lt;/p&gt;

&lt;h3&gt;
  
  
  Sensitivity check: a different length curve
&lt;/h3&gt;

&lt;p&gt;The reader asked the mirror question. Leave-one-source-out tests whether the ranking depends on individual texts. It does not test whether it depends on the curve I chose for length. The model adjusts for length with a single log2(tokens) term. If fiction only comes out on top under that one choice, the ranking belongs to the curve, not the data.&lt;/p&gt;

&lt;p&gt;So I kept the source grouping and the grouped bootstrap, and swapped only the length term. Six alternatives: no length term at all (raw genre means), tokens as a straight line, square root of tokens, log2 plus its square, the four size buckets as dummies, and a model where each genre gets its own length slope, read at the median length.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Length term&lt;/th&gt;
&lt;th&gt;R^2&lt;/th&gt;
&lt;th&gt;Fiction vs explainer (z)&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;th&gt;Fiction first in re&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none (raw means)&lt;/td&gt;
&lt;td&gt;0.22&lt;/td&gt;
&lt;td&gt;+3.22&lt;/td&gt;
&lt;td&gt;1.51 to 4.94&lt;/td&gt;
&lt;td&gt;97.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;log2(tokens), as published&lt;/td&gt;
&lt;td&gt;0.44&lt;/td&gt;
&lt;td&gt;+2.38&lt;/td&gt;
&lt;td&gt;1.26 to 3.55&lt;/td&gt;
&lt;td&gt;97.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tokens, linear&lt;/td&gt;
&lt;td&gt;0.46&lt;/td&gt;
&lt;td&gt;+2.30&lt;/td&gt;
&lt;td&gt;1.24 to 3.41&lt;/td&gt;
&lt;td&gt;96.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqrt(tokens)&lt;/td&gt;
&lt;td&gt;0.45&lt;/td&gt;
&lt;td&gt;+2.34&lt;/td&gt;
&lt;td&gt;1.27 to 3.47&lt;/td&gt;
&lt;td&gt;97.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;log2 + log2 squared&lt;/td&gt;
&lt;td&gt;0.46&lt;/td&gt;
&lt;td&gt;+2.32&lt;/td&gt;
&lt;td&gt;1.21 to 3.45&lt;/td&gt;
&lt;td&gt;96.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;size bucket dummies&lt;/td&gt;
&lt;td&gt;0.43&lt;/td&gt;
&lt;td&gt;+2.59&lt;/td&gt;
&lt;td&gt;1.31 to 3.84&lt;/td&gt;
&lt;td&gt;97.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;log2, per-genre slope, at median length&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;+1.98&lt;/td&gt;
&lt;td&gt;0.88 to 2.78&lt;/td&gt;
&lt;td&gt;87.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fiction ranks first under every specification, and personal narrative ranks second under every one. The fiction difference sits between 1.98 and 2.59 z across the six adjusted fits. The per-genre-slope model gives the smallest value, because it lets fiction's own slope absorb part of the gap, and even there the interval stays clear of zero. The only rank changes anywhere in the table are among the genres that sit within 0.15 z of explainers: how-to guide crosses zero under the linear and square-root terms (+0.01 to -0.01), which again just restates that its interval includes zero. Dropping the length term entirely inflates fiction to 3.22 and flips the sign of four small genres, which is the length confound the original comment pointed out, now visible as a row in the table.&lt;/p&gt;

&lt;p&gt;Same caveat as before. This probes whether the estimate is fragile to the fitted curve, not whether 10 fiction sources speak for fiction in general. (Added October 20 after the reader asked to vary the length adjustment while keeping the source grouping. Script: breakdown.py v5, log breakdown_v5.log.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Bottom line
&lt;/h3&gt;

&lt;p&gt;Same result, tighter error bars, and two doubts that used to be arguments are now data. Thanks to the readers who pushed on this.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>ollama</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Can a Local LLM Wash Out a Watermark Without Washing Out the Meaning? I Tested 300 Rewrites</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Fri, 02 Oct 2026 18:49:07 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/can-a-local-llm-wash-out-a-watermark-without-washing-out-the-meaning-i-tested-300-rewrites-15jb</link>
      <guid>https://dev.to/vadim_albarov/can-a-local-llm-wash-out-a-watermark-without-washing-out-the-meaning-i-tested-300-rewrites-15jb</guid>
      <description>&lt;p&gt;AI text watermarks hide a signal in &lt;em&gt;which words&lt;/em&gt; a model picks. A common idea is that you can remove that signal by running the text through another model: "rewrite this in your own words."&lt;/p&gt;

&lt;p&gt;That raises a simple question. &lt;strong&gt;If you rewrite a text enough to break its word patterns, do you still keep its meaning?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I tested it on a single GPU with 16 GB of VRAM. In short: &lt;strong&gt;yes, mostly — but the model you pick matters a lot, and there is a real trade-off between "changed a lot" and "kept everything."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Here is the pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Claude Opus 5.5 wrote 100 source texts.&lt;/strong&gt; There were 10 genres (news, technical explainers, science, opinion, personal stories, marketing, how-to guides, business memos, history, and short fiction), with 10 topics each. Lengths were from about 190 to 900 words. Every text was packed with checkable details: names, numbers, dates, steps, and plot events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three local models rewrote every text&lt;/strong&gt; with &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; on a GPU with 16 GB VRAM:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;qwen3:4b-instruct&lt;/code&gt; (small)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gemma3:12b&lt;/code&gt; (mid)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;qwen3:14b&lt;/code&gt; (upper mid, thinking mode off)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Fable 5.1 judged all 300 rewrites blind.&lt;/strong&gt; Each packet held the original and three rewrites labeled A/B/C in random order. The judge listed every lost, changed, or added fact, and gave each rewrite a score from 1 to 5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I measured how much the wording changed&lt;/strong&gt; by counting how many of the original's word n-grams survive in the rewrite.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All models used the same prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rewrite the text below completely in your own words.
- Change the vocabulary and the sentence structure; do not copy phrases.
- Keep ALL of the meaning: every fact, name, number, date, step, argument, and plot event.
- Do not add new information, comments, or opinions.
- Keep the same genre, tone, and roughly the same length. Keep the title line, rewritten.
Output only the rewritten text, nothing else.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Settings: temperature 0.7, &lt;code&gt;num_ctx&lt;/code&gt; 8192, one run per text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why n-grams are a fair proxy for "watermark removed"
&lt;/h2&gt;

&lt;p&gt;Most text watermarks (green-list schemes like Kirchenbauer et al., and Google's SynthID-Text) work at the token level. While the model writes, they nudge it toward certain tokens based on the tokens just before. A detector then counts how often these "preferred" token pairs or sequences appear.&lt;/p&gt;

&lt;p&gt;So if most of the original word sequences are gone, most of the signal is gone too. I did &lt;strong&gt;not&lt;/strong&gt; run a watermark detector, because I have no access to one for these texts. Treat the n-gram numbers as a proxy, not a proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Meaning, by model
&lt;/h3&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;Avg score (1–5)&lt;/th&gt;
&lt;th&gt;"Preserved"&lt;/th&gt;
&lt;th&gt;"Mostly preserved"&lt;/th&gt;
&lt;th&gt;"Not preserved"&lt;/th&gt;
&lt;th&gt;Same understanding for the reader&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:14b&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.68&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;82%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma3:12b&lt;/td&gt;
&lt;td&gt;4.09&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:4b-instruct&lt;/td&gt;
&lt;td&gt;3.76&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;All 300&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.18&lt;/td&gt;
&lt;td&gt;48%&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Same understanding" was a yes/no question to the judge: &lt;em&gt;would a reader of the rewrite come away with the same understanding as a reader of the original?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When the judge ranked the three rewrites of each text (still blind):&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;Ranked best&lt;/th&gt;
&lt;th&gt;Ranked worst&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:14b&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;76&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma3:12b&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:4b-instruct&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Average errors per rewrite, as listed by the judge:&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;Facts lost&lt;/th&gt;
&lt;th&gt;Facts changed&lt;/th&gt;
&lt;th&gt;Things added&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:14b&lt;/td&gt;
&lt;td&gt;0.59&lt;/td&gt;
&lt;td&gt;0.96&lt;/td&gt;
&lt;td&gt;0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma3:12b&lt;/td&gt;
&lt;td&gt;1.42&lt;/td&gt;
&lt;td&gt;2.04&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:4b-instruct&lt;/td&gt;
&lt;td&gt;1.22&lt;/td&gt;
&lt;td&gt;2.94&lt;/td&gt;
&lt;td&gt;0.38&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Wording change, by model
&lt;/h3&gt;

&lt;p&gt;"Kept" means the share of the original's n-grams that still appear in the rewrite. Lower means a heavier rewrite.&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;Words kept (1-gram)&lt;/th&gt;
&lt;th&gt;2-grams kept&lt;/th&gt;
&lt;th&gt;3-grams kept&lt;/th&gt;
&lt;th&gt;5-grams kept&lt;/th&gt;
&lt;th&gt;Longest copied run (words)&lt;/th&gt;
&lt;th&gt;Length vs original&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gemma3:12b&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;34%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;11.7&lt;/td&gt;
&lt;td&gt;1.12×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:4b-instruct&lt;/td&gt;
&lt;td&gt;64%&lt;/td&gt;
&lt;td&gt;37%&lt;/td&gt;
&lt;td&gt;23%&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;td&gt;12.4&lt;/td&gt;
&lt;td&gt;1.05×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:14b&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;td&gt;37%&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;td&gt;20.7&lt;/td&gt;
&lt;td&gt;1.07×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The trade-off
&lt;/h3&gt;

&lt;p&gt;Put the two tables side by side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;qwen3:14b&lt;/strong&gt; kept the meaning best by far, but it also kept the most of the original wording. About 1 in 5 five-word sequences survived, and on average it copied a run of about 21 words straight from the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gemma3:12b&lt;/strong&gt; changed the wording the most (only 8% of 5-grams survived) and still scored 4.09. Almost all its rewrites (98%) gave the reader the same understanding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;qwen3:4b-instruct&lt;/strong&gt; changed the wording almost as much as Gemma, but it was the least accurate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Across all 300 rewrites, the correlation between "meaning score" and "5-grams kept" was &lt;strong&gt;0.36&lt;/strong&gt;. So yes, rewrites that copy more wording tend to keep more meaning. But inside each model the link was weak (0.03–0.22). Most of the difference comes from &lt;em&gt;which model&lt;/em&gt; you use, not from how hard a given model happens to rewrite a given text.&lt;/p&gt;

&lt;h3&gt;
  
  
  What kinds of mistakes?
&lt;/h3&gt;

&lt;p&gt;The type of error mattered more than the count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;qwen3:14b&lt;/strong&gt; mostly made tiny changes in nuance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"two placebo pills work better than one" was hedged to "may be more effective"&lt;/li&gt;
&lt;li&gt;"ransomware" was widened to "malware attacks"&lt;/li&gt;
&lt;li&gt;an overall risk of "moderate" became "average"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;gemma3:12b&lt;/strong&gt; made the wording vaguer or swapped in a near-synonym:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Most houseplants" became "Many indoor plants"&lt;/li&gt;
&lt;li&gt;"hiring manager" became "recruiter"&lt;/li&gt;
&lt;li&gt;Kubernetes init containers that "run to completion, one after another" became a &lt;em&gt;single&lt;/em&gt; init container that "executes a sequence of tasks". That one is a real technical error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a typical example. Original:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SafeNest Products announced on Monday that it is recalling about 86,000 infant car seats sold in the United States because the harness buckle may fail to latch fully.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Gemma:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On Monday, SafeNest Products declared a recall of roughly 86,000 car seats intended for infants, distributed across the United States, due to a potential issue with the harness buckle's functionality.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every fact is still there, but "may fail to latch fully" became "a potential issue with ... functionality." It reads fine, but it says less.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;qwen3:4b-instruct&lt;/strong&gt; made the dangerous mistakes: wrong names and wrong numbers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;renamed products&lt;/strong&gt;: "Quietwave Q7" headphones became "SilentHush S7" all through the text, and the "Elevate Pro Dual-Motor" desk became "Rise Pro Dual-Drive."&lt;/li&gt;
&lt;li&gt;It garbled numbers: a desk height range "from 150 to 200 centimeters" became "from 150 to 20."&lt;/li&gt;
&lt;li&gt;It changed amounts: British tea imports of "millions of pounds" a year became "hundreds of thousands of pounds."&lt;/li&gt;
&lt;li&gt;It cut a farmer's name, "Marcos Ribeiro," to "Marcos Ribe."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rough keyword search over the judge's notes found number-related changes in about &lt;strong&gt;31 of 100&lt;/strong&gt; qwen3:4b rewrites, compared with about 11 for Gemma and 3 for qwen3:14b. This count is approximate, but the gap is clear.&lt;/p&gt;

&lt;p&gt;All 3 rewrites judged "not preserved" in the whole test came from qwen3:4b. Two were the renamed products, and the third was a short story whose ending stopped making sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  By genre and length
&lt;/h3&gt;

&lt;p&gt;The genre made less difference than I expected. The average score ranged from 4.03 (marketing copy) to 4.40 (science writing). Marketing copy had the most "not preserved" verdicts. Product names and spec numbers are exactly where the small model slips, and those details &lt;em&gt;are&lt;/em&gt; the meaning of marketing copy. Business memos and technical explainers were most often fully "preserved" (77–80%).&lt;/p&gt;

&lt;p&gt;Length did not hurt. The 750–900 word texts scored about the same as, or a little better than, the short ones (4.33 vs 4.19). With an 8K context, all three models handled 900 words easily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed
&lt;/h3&gt;

&lt;p&gt;Total time for 100 rewrites on the 16 GB GPU:&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;Total&lt;/th&gt;
&lt;th&gt;Per text&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:4b-instruct&lt;/td&gt;
&lt;td&gt;11 min&lt;/td&gt;
&lt;td&gt;~7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3:14b&lt;/td&gt;
&lt;td&gt;25 min&lt;/td&gt;
&lt;td&gt;~15 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma3:12b&lt;/td&gt;
&lt;td&gt;25 min&lt;/td&gt;
&lt;td&gt;~15 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  So — does the rewrite keep the meaning?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;With a good 12–14B model: yes.&lt;/strong&gt; Readers would get the same understanding from 98–100% of the rewrites, and none were judged "not preserved." The remaining issues are small shifts in nuance or precision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With a 4B model: usually, but you can't trust the details.&lt;/strong&gt; One in five rewrites changed what a reader would understand. Names and numbers are the weak spot, and they are often the facts that matter most.&lt;/p&gt;

&lt;p&gt;And there is a trade-off you can't ignore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you want &lt;strong&gt;maximum fidelity&lt;/strong&gt;, &lt;code&gt;qwen3:14b&lt;/code&gt; is the clear winner. But it keeps more of the original wording, so if your goal is to break token-level patterns, it does the weakest job.&lt;/li&gt;
&lt;li&gt;If you want &lt;strong&gt;maximum change in wording with good meaning&lt;/strong&gt;, &lt;code&gt;gemma3:12b&lt;/code&gt; is the sweet spot. Only 8% of 5-grams survive, and the reader gets the same understanding 98% of the time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No real watermark detector.&lt;/strong&gt; N-gram overlap is a proxy. Some watermark research claims robustness to paraphrase, and semantic-level watermarks would not be hurt by word changes at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One judge, and it is an LLM.&lt;/strong&gt; Fable 5.1 was strict and specific (it listed things like "price changed from X to Y"), but I did not check it against human ratings. The generator and the judge are both Claude models, which could add some bias toward Claude-like phrasing. That should affect all three local models equally, though.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One run per text&lt;/strong&gt; at temperature 0.7. Another seed would give somewhat different rewrites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English only&lt;/strong&gt;, synthetic texts, one prompt. A stricter prompt ("never change any name or number") would probably help the 4B model.&lt;/li&gt;
&lt;li&gt;The keyword counts of name and number errors are approximate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next time: SynthID?
&lt;/h2&gt;

&lt;p&gt;This test answered only half of the question. It showed that a good local model keeps the meaning, but it only &lt;em&gt;estimated&lt;/em&gt; watermark removal from n-gram overlap.&lt;/p&gt;

&lt;p&gt;A real check would use &lt;strong&gt;SynthID-Text&lt;/strong&gt;, Google's open-source text watermark. Would rewriting with a local model really remove a watermark that a detector can see?&lt;/p&gt;

&lt;p&gt;Should I run that test next? Tell me in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>ollama</category>
      <category>discuss</category>
    </item>
    <item>
      <title>How Much Does Your Lunch Break Cost in Claude Code Tokens?</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Thu, 01 Oct 2026 15:06:06 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/how-much-does-your-lunch-break-cost-in-claude-code-tokens-4k0o</link>
      <guid>https://dev.to/vadim_albarov/how-much-does-your-lunch-break-cost-in-claude-code-tokens-4k0o</guid>
      <description>&lt;p&gt;You step away for lunch with a big Claude Code session open. You come back, type one message, and continue. Nothing looks different. But that one message may have cost 20 times more than the one before it.&lt;/p&gt;

&lt;p&gt;I wanted a real number, so I scanned a month of my own Claude Code logs. The short answer: a typical break past the one-hour mark cost me about 125k–135k tokens of cold cache writes. Over the month, these restarts added about 15% to my input usage.&lt;/p&gt;

&lt;p&gt;This post explains why it happens, shows the script I used, and lists what I changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How prompt caching works in Claude Code
&lt;/h2&gt;

&lt;p&gt;Every turn in a Claude Code session sends the whole conversation to the model again: system prompt, tool definitions, project files you read, tool output, everything. A long session can carry 100k–400k tokens of context.&lt;/p&gt;

&lt;p&gt;Prompt caching makes this affordable. After the first request, the stable prefix of the prompt is stored in a cache. Later requests read it from the cache instead of processing it again. In API pricing terms, relative to the base input price:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token type&lt;/th&gt;
&lt;th&gt;Relative price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Uncached input&lt;/td&gt;
&lt;td&gt;1.0x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache write, 1-hour TTL&lt;/td&gt;
&lt;td&gt;2.0x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache read&lt;/td&gt;
&lt;td&gt;0.1x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A warm turn reads the context at 0.1x. A cold turn writes the whole context again at 2.0x. That is where the &lt;strong&gt;roughly 20x&lt;/strong&gt; comes from.&lt;/p&gt;

&lt;p&gt;The cache has a time to live (TTL). In my Claude Code sessions it is &lt;strong&gt;one hour&lt;/strong&gt;, and each request resets the timer. Tool calls count as requests, so an agent working on its own keeps the cache warm. The risk is when &lt;em&gt;you&lt;/em&gt; are the slow part: a meeting, lunch, the end of the day.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: the 1-hour TTL is what my sessions used. The default API TTL is 5 minutes, and Claude Code can drop to 5 minutes in some cases, for example usage overage. The script below reports which TTL each write used, so you can check your own.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where the data lives
&lt;/h2&gt;

&lt;p&gt;Claude Code stores each session as a JSONL file under &lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/&amp;lt;session-id&amp;gt;.jsonl&lt;/code&gt;. Every assistant turn includes a &lt;code&gt;usage&lt;/code&gt; block. Trimmed:&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;"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;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T13:28:05.993Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"msg_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usage"&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;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache_creation_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32912&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache_read_input_tokens"&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;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;211&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache_creation"&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;"ephemeral_1h_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32912&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ephemeral_5m_input_tokens"&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="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;Three fields matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cache_read_input_tokens&lt;/code&gt;: context served from the cache (cheap).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache_creation_input_tokens&lt;/code&gt;: context written to the cache (expensive).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache_creation.ephemeral_1h_input_tokens&lt;/code&gt; / &lt;code&gt;ephemeral_5m_input_tokens&lt;/code&gt;: which TTL the write used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a normal warm turn, reads are large and writes are small (just the new tool output). A cache miss looks the other way round: &lt;strong&gt;a large write, little or no read&lt;/strong&gt;. If the previous turn in the same file was more than an hour earlier, the idle gap is the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;The script walks every session file, removes duplicate messages by ID, and flags turns where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cache_creation_input_tokens&lt;/code&gt; is at least a threshold (default 50,000), and&lt;/li&gt;
&lt;li&gt;the write is larger than the read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It records the gap since the previous turn, groups the misses by gap length, and estimates a weighted cost share using the prices above.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Click to see the full script (100 lines)
  &lt;br&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;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="n"&gt;ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/.claude/projects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;MIN_MISS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;50_000&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&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;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;misses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;totals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;per_project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;prev_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;seen_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&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="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="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&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;msg&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&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;mid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;seen_ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mid&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="nf"&gt;parse_ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_read_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_creation_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="n"&gt;cc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_creation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
            &lt;span class="n"&gt;w1h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cc&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral_1h_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="n"&gt;w5m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cc&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral_5m_input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&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="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turns&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;totals&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="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;
            &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;
            &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;
            &lt;span class="n"&gt;per_project&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;project&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="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;
            &lt;span class="n"&gt;per_project&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;
            &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&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;prev_ts&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;prev_ts&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;MIN_MISS&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;misses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&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;gap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                   &lt;span class="n"&gt;w1h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;w1h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w5m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;w5m&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;prev_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;

&lt;span class="n"&gt;misses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fmt_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;h&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="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;m &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cache misses &amp;gt;= &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MIN_MISS&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; created tokens, read &amp;lt; created&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gap&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&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="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; sess&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;misses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w1h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w5m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;fmt_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gap&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&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="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&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&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;key&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;session start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;5m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5m-1h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&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;else&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1h-24h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;24h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&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="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&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="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Misses by idle gap before the request:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session start&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;&amp;lt;5m&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;5m-1h&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;1h-24h&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;&amp;gt;24h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; misses  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens 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;miss_tok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;misses&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;All assistant turns: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;turns&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  cache read tokens    : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;totals&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="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  cache created tokens : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;miss_tok&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; in the misses listed above)&lt;/span&gt;&lt;span class="sh"&gt;"&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  uncached input tokens: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# cost weights: read 0.1, 1h write 2.0, raw 1.0
&lt;/span&gt;&lt;span class="n"&gt;w_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;totals&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="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;
&lt;span class="n"&gt;w_created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;
&lt;span class="n"&gt;w_raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="n"&gt;w_miss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;miss_tok&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;
&lt;span class="n"&gt;tot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w_read&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w_created&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;w_raw&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Weighted input cost share (read=0.1, write=2.0, raw=1.0):&lt;/span&gt;&lt;span class="sh"&gt;"&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  cache reads : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w_read&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tot&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;5.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  cache writes: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w_created&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tot&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;5.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%   of which listed misses: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w_miss&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tot&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;5.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  raw input   : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w_raw&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tot&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;5.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Top projects by cache-write tokens:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;kv&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])[:&lt;/span&gt;&lt;span class="mi"&gt;8&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; written &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  read &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&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="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Run it with an optional threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python cache_misses.py          &lt;span class="c"&gt;# default 50,000&lt;/span&gt;
python cache_misses.py 30000    &lt;span class="c"&gt;# lower threshold&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My results
&lt;/h2&gt;

&lt;p&gt;The data covers 2026-08-31 to 2026-10-01: 2,410 assistant turns across five projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  The misses
&lt;/h3&gt;

&lt;p&gt;Restarts after an idle gap of over one hour, threshold 50k:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Idle gap&lt;/th&gt;
&lt;th&gt;Tokens re-written&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.1h&lt;/td&gt;
&lt;td&gt;170k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.1h&lt;/td&gt;
&lt;td&gt;80k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.2h&lt;/td&gt;
&lt;td&gt;137k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.2h&lt;/td&gt;
&lt;td&gt;77k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.2h&lt;/td&gt;
&lt;td&gt;193k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.3h&lt;/td&gt;
&lt;td&gt;80k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.4h&lt;/td&gt;
&lt;td&gt;142k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.5h&lt;/td&gt;
&lt;td&gt;58k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.6h&lt;/td&gt;
&lt;td&gt;110k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.6h&lt;/td&gt;
&lt;td&gt;164k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.2h&lt;/td&gt;
&lt;td&gt;148k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.5h&lt;/td&gt;
&lt;td&gt;53k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.6h&lt;/td&gt;
&lt;td&gt;171k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.6h&lt;/td&gt;
&lt;td&gt;103k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.8h&lt;/td&gt;
&lt;td&gt;131k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15.1h&lt;/td&gt;
&lt;td&gt;124k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18.7h&lt;/td&gt;
&lt;td&gt;233k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;61.8h&lt;/td&gt;
&lt;td&gt;476k&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Idle restarts over 1h&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens re-written cold&lt;/td&gt;
&lt;td&gt;2.65M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean per restart&lt;/td&gt;
&lt;td&gt;147k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median per restart&lt;/td&gt;
&lt;td&gt;134k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median for gaps of 1.1h–1.6h (10 restarts)&lt;/td&gt;
&lt;td&gt;123k&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mean is pulled up by one outlier: a session resumed after a 2.5-day weekend that re-wrote 476k tokens in one request. The median is the more honest "cost of a break".&lt;/p&gt;

&lt;h3&gt;
  
  
  The pattern
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Most gaps barely passed the limit.&lt;/strong&gt; 10 of the 18 were between 1.1 and 1.6 hours. These are lunch breaks and meetings, not abandoned sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One stale session cost as much as three or four normal breaks.&lt;/strong&gt; Resuming a huge session after days away is the worst case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session starts are a separate, fixed cost.&lt;/strong&gt; At a 30k threshold the script also shows 14 session starts, each 31k–36k tokens. That is the startup context: system prompt, &lt;code&gt;CLAUDE.md&lt;/code&gt;, memory, skills, and MCP tool definitions. Every new session pays it once, and no timing habit removes it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The cost share
&lt;/h3&gt;

&lt;p&gt;Weighted by relative price across all 2,410 turns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;Weighted share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache reads&lt;/td&gt;
&lt;td&gt;234M&lt;/td&gt;
&lt;td&gt;55%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache writes, normal incremental&lt;/td&gt;
&lt;td&gt;6.8M&lt;/td&gt;
&lt;td&gt;32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache writes, idle misses&lt;/td&gt;
&lt;td&gt;2.7M&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uncached input&lt;/td&gt;
&lt;td&gt;82k&lt;/td&gt;
&lt;td&gt;0.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Idle misses were 13% of the total weighted input. Put another way: without them, input cost would have been 87% of what it was, so the misses added about &lt;strong&gt;15%&lt;/strong&gt; on top.&lt;/p&gt;

&lt;p&gt;The 32% for normal writes is the ordinary cost of new tool output going into the cache each turn. Timing does not change it. Only the 13% slice can be avoided.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Come back within the hour when I can.&lt;/strong&gt; Most of my misses were 5 to 35 minutes past the limit. If I know I will be back soon, I make the next message the first thing I do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Run &lt;code&gt;/compact&lt;/code&gt; before a long break.&lt;/strong&gt; If I will be away for more than an hour and the session is large, I compact while the cache is still warm. The summary is cheap to produce on a warm cache. The restart after the break then re-writes a small summary, not 150k tokens of history, and every turn after that is cheaper too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Do not revive stale sessions.&lt;/strong&gt; For anything older than a day, I start a fresh session with a short brief: goal, current state, key files. That costs about 35k tokens of startup context, not 400k+ of old history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Do not bother with keep-alive pings.&lt;/strong&gt; Sending a "heartbeat" message every hour does keep the cache warm, and each one costs only about 0.1x of the context. On paper it wins for gaps up to about 20 hours (20 pings at 0.1x equal one cold write at 2.0x). In practice it adds noise to the conversation, it stops when the machine sleeps, and it keeps a large context alive that you may not need. &lt;code&gt;/compact&lt;/code&gt; is simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan differences.&lt;/strong&gt; On a subscription plan, tokens count against usage limits, not a bill. The relative weights still show what eats your limits faster, but your plan may not count tokens exactly like API prices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The weights are an estimate.&lt;/strong&gt; Read = 0.1, 1h write = 2.0, uncached = 1.0 follows published API pricing ratios. Output tokens are left out on purpose, because they do not depend on caching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The heuristic is simple.&lt;/strong&gt; "Write &amp;gt; read and write ≥ 50k" catches idle restarts well. It also catches a few non-idle events, such as one write under 5 minutes in my data, so always look at the gap column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One user, one month.&lt;/strong&gt; These are numbers from my own work: several infrastructure and app repos, long agentic sessions, normal office hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it on your own logs
&lt;/h2&gt;

&lt;p&gt;Save the script, run it, and look at two things: the &lt;code&gt;1h-24h&lt;/code&gt; bucket and the &lt;code&gt;&amp;gt;24h&lt;/code&gt; bucket. If they hold a large share of your cache writes, a habit change will save you more than any prompt tweak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over to you:&lt;/strong&gt; is saving ~15% of tokens worth changing how you work? Or is the extra attention not worth it? I'd like to know how you handle long sessions and breaks.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;These numbers are averages from one month of my own work. Your numbers could vary.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Typos don't break LLM prompts. One missing quote mark does.</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Sun, 27 Sep 2026 21:02:46 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/typos-dont-break-llm-prompts-one-missing-quote-mark-does-d7d</link>
      <guid>https://dev.to/vadim_albarov/typos-dont-break-llm-prompts-one-missing-quote-mark-does-d7d</guid>
      <description>&lt;p&gt;Everyone who types prompts has wondered the same thing. Does the model care that I wrote "teh" or forgot a comma? Should I clean up my prompt before hitting enter?&lt;/p&gt;

&lt;p&gt;I ran a test to find out. The short answer: stop fixing your spelling. Start checking your quotes and colons.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tested
&lt;/h2&gt;

&lt;p&gt;I wrote a set of tasks with short, checkable answers. Arithmetic, extraction from a passage, list filtering with "except" and "not", a Python snippet's output, counting words inside quotes, and so on. Then I broke the prompts in controlled ways and sent every version to every model, several times each, in a fresh session every time.&lt;/p&gt;

&lt;p&gt;Two prompt sets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easy set.&lt;/strong&gt; 12 one-line tasks, each hand-written at five error levels, from clean to "heavy typos plus broken punctuation".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard set.&lt;/strong&gt; 9 long prompts (200 to 450 words) where the question depends on a detail buried in the text. Here I separated the two kinds of damage:

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Spelling noise&lt;/em&gt;: a script misspells 35% or 70% of the words, but never touches the words the answer depends on.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Structural break&lt;/em&gt;: the spelling is perfect, but exactly one punctuation mark that carries meaning is wrong. A missing closing quote. A colon dropped before a list. A comma moved around "except". &lt;code&gt;9.45&lt;/code&gt; instead of &lt;code&gt;9:45&lt;/code&gt;. &lt;code&gt;resign&lt;/code&gt; instead of &lt;code&gt;re-sign&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Two realistic profiles on top: &lt;em&gt;non-native grammar&lt;/em&gt; (articles and verb forms off, spelling fine) and &lt;em&gt;voice-to-text&lt;/em&gt; (no punctuation, no capitals, numbers spelled out, "resign" for "re-sign").&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Models: Claude Opus 5, Opus 5.5, Sonnet 5, Haiku 4.5, Fable 5.1 (all at low and medium effort), plus seven local models through Ollama: gemma4, qwen3 14B, phi4 14B, granite 4.2, ornith-1.5 9B, llama 3.1 8B, mistral 7B.&lt;/p&gt;

&lt;p&gt;Total: about 4,900 sessions. An answer counts as correct if the ideal answer appears in the reply, even with an explanation attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 1: spelling and grammar cost nothing
&lt;/h2&gt;

&lt;p&gt;Here is the hard set. Each cell is the percent of correct answers.&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;clean&lt;/th&gt;
&lt;th&gt;70% typos&lt;/th&gt;
&lt;th&gt;non-native grammar&lt;/th&gt;
&lt;th&gt;voice-to-text&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;one punctuation break&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5.1&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 4.5&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;77&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;granite4.2&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;phi4 14B&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma4&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3 14B&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ornith-1.5 9B&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama 3.1 8B&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mistral 7B&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the Claude rows first. With 70% of the words misspelled, every one of them scored the same as on clean text. Non-native grammar: 100% across the board. This is the prompt that scored 100%:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;shop sellin pens 3dolar each an notbooks 5 dolar,each tom buys 4pen an 2notbook.how much he pay totaly,anser numbr only&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now read the last column. One punctuation mark, with perfect spelling everywhere else, and the same models lose 8 to 23 points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 2: which punctuation mark breaks which model
&lt;/h2&gt;

&lt;p&gt;Not every break matters. Most were recovered from context by every Claude model. Two were not.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;break&lt;/th&gt;
&lt;th&gt;Opus 5&lt;/th&gt;
&lt;th&gt;Opus 5.5&lt;/th&gt;
&lt;th&gt;Fable&lt;/th&gt;
&lt;th&gt;Haiku&lt;/th&gt;
&lt;th&gt;Sonnet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;missing closing quote&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;colon dropped before a list&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comma moved around "except"&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;European decimals (1.500 at 2,50)&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9.45 instead of 9:45&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;managers' → manager's&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;resign instead of re-sign&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comma dropped in "not closed, and assigned to Lee"&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The missing quote.&lt;/strong&gt; The task: count how many times "report" appears inside a quoted paragraph, not in the text after it. Remove the closing quote and every model counts the three extra "report"s after the paragraph. Answer 8 instead of 5. Opus 5.5 even wrote "the closing quote is missing, so I counted all" and still gave the wrong number. It noticed the problem and followed the broken structure anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dropped colon.&lt;/strong&gt; "Give the plural form of each word below keeping the order mouse child foot analysis reply with the plurals only comma separated". Sonnet answered &lt;code&gt;mice, children, feet, analyses, replies&lt;/code&gt;. Every local model did the same. The word boundary between instruction and data was gone, so "reply" became data.&lt;/p&gt;

&lt;p&gt;Everything else was recovered, including the genuinely ambiguous one. "List the tickets that are not closed and assigned to Lee" can mean two things once the comma is gone. All five Claude models picked the intended reading every time, because the sentence started with "For Lee's standup".&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 3: dictation is a different story
&lt;/h2&gt;

&lt;p&gt;The voice-to-text column is where the small models fall apart. gemma4 goes from 78 to 44. granite from 93 to 56. Two things do the damage: spelled-out numbers ("three hundred forty pallets across twenty two trailers") and, again, the missing colon before a list. Every local model scored 0% on the dictated list task.&lt;/p&gt;

&lt;p&gt;The frontier models mostly shrug it off, with one honest exception. "fifteen hundred units at two fifty each": Sonnet read "two fifty" as $250 in six trials out of six and answered 375,000 instead of 3,750. Fable did it once. Haiku turned "extension two zero one" into 2001 once. These are not model bugs. Spoken numbers are ambiguous, and the model has to guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that did not help
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Effort level.&lt;/strong&gt; Low versus medium made no difference for any model on any task. Structural breaks are not a reasoning problem, so more thinking does not fix them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning the model.&lt;/strong&gt; I prepended "the message below may contain spelling, grammar, and punctuation mistakes, answer according to the writer's likely intent". Zero effect on Opus 5, Haiku, and Sonnet. A few points on Opus 5.5 and Fable. Mixed or negative on the local models.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What degrades first: the format, not the answer
&lt;/h2&gt;

&lt;p&gt;Sloppy prompts get slightly sloppier replies before they get wrong ones. Asked for "the number only", Fable and Opus 5.5 add a line of working in about 20% of runs on any prompt. Haiku, on the easy set, drifted into "Yes. If all bloops are razzies..." explanations as the grammar got worse, and at the heaviest level refused four times out of eight because "bloops and razzies are not real words". With clean grammar it had answered the same nonsense-word question fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Don't fix spelling or grammar. The model does not care, and neither should you.&lt;/li&gt;
&lt;li&gt;Do check the punctuation that carries structure: closing quotes, the colon before a list, commas around "except" and "not", and anything separating your instructions from your data.&lt;/li&gt;
&lt;li&gt;If you dictate prompts, re-read the numbers and the list boundaries before sending.&lt;/li&gt;
&lt;li&gt;Small local models are much less forgiving. If you run a 7B to 14B model, treat every delimiter as load-bearing and avoid dictation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;Twelve short tasks and nine long ones, three trials per cell, one random seed for the typo generator. The voice-to-text prompts are hand-simulated, not real speech engine output. Claude models were called through the Claude Code CLI with a minimal system prompt and tools disabled. Local models ran at Ollama defaults with thinking off. The harness and every prompt and reply are in the repo linked below, so you can rerun it with your own tasks.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Repo with the harness, all prompts, and every raw reply: &lt;a href="https://github.com/valbarov/prompt-noise-test" rel="noopener noreferrer"&gt;github.com/valbarov/prompt-noise-test&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>prompting</category>
      <category>testing</category>
    </item>
    <item>
      <title>12 AWS Defaults That Ship Insecure (and the One Line That Fixes Each)</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Sun, 27 Sep 2026 01:19:02 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/12-aws-defaults-that-ship-insecure-and-the-one-line-that-fixes-each-127f</link>
      <guid>https://dev.to/vadim_albarov/12-aws-defaults-that-ship-insecure-and-the-one-line-that-fixes-each-127f</guid>
      <description>&lt;p&gt;Every AWS default answers exactly one question: will the tutorial work?&lt;/p&gt;

&lt;p&gt;No surprise bill. No failed API call. No "access denied" on step three. That is a great default for a tutorial. It is a terrible default for the thing you spun up "just for staging" that is now production, on the day someone asks who downloaded that bucket in March.&lt;/p&gt;

&lt;p&gt;I run infrastructure for a healthcare company. People with clipboards read my configs. Somewhere around my third Terraform module I noticed my job had a shape, and the shape was not "design clever things". It was flipping the same switches AWS had left in the wrong position, over and over. So I wrote them down.&lt;/p&gt;

&lt;p&gt;Here are the twelve, ranked by how likely each one is to hurt you. One rule of thumb before we start: &lt;code&gt;checkov&lt;/code&gt; and &lt;code&gt;tfsec&lt;/code&gt; check what you wrote. Most of this list is about what you did not write.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. CloudTrail remembers 90 days, then forgets
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; you get "Event history": 90 days of management events, viewable in the console. No trail to S3 unless you create one. And S3 object reads and writes (data events) are not logged at all, even when a trail exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; a credential gets phished in March. In September someone asks what it touched. The window scrolled off in June, and even inside the window "did they download the sensitive bucket" was never recorded. Your answer is a shrug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudtrail"&lt;/span&gt; &lt;span class="s2"&gt;"audit"&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="s2"&gt;"org-audit"&lt;/span&gt;
  &lt;span class="nx"&gt;s3_bucket_name&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;is_multi_region_trail&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;enable_log_file_validation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;event_selector&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;read_write_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"All"&lt;/span&gt;
    &lt;span class="nx"&gt;data_resource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS::S3::Object"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::my-sensitive-bucket/"&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;Data events cost real money at volume. Scope them on purpose, not by forgetting.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. RDS storage is unencrypted, and you cannot fix that later
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; &lt;code&gt;storage_encrypted = false&lt;/code&gt; in Terraform and in the API. The console nudges you. Code does not. And encryption can only be enabled at creation. Later means snapshot, encrypted copy, restore, cutover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; this is the worst one on the list because it is irreversible in place. Staging quietly becomes production, it always does, and eighteen months later a review finds your primary database in plaintext. The remediation is a maintenance window on your busiest system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;storage_encrypted&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;kms_key_id&lt;/span&gt;        &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_kms_key&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my modules this is hardcoded, not a variable. A security invariant that callers can toggle off is not an invariant. It is a default waiting to come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your Postgres accepts plaintext connections
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; on RDS for PostgreSQL 14 and earlier, &lt;code&gt;rds.force_ssl&lt;/code&gt; is 0. The server supports TLS. It also cheerfully accepts connections without it. AWS flipped this to 1 for PostgreSQL 15+, which tells you what AWS thinks of the old default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; any client on &lt;code&gt;sslmode=prefer&lt;/code&gt; (the libpq default) falls back to plaintext if the handshake hiccups. Nothing fails. Nothing logs. Your transmission security now depends on every developer, sidecar, and ad hoc &lt;code&gt;psql&lt;/code&gt; from a bastion remembering a flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; enforce it at the engine so client config stops mattering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;parameter&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="s2"&gt;"rds.force_ssl"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Your load balancer still shakes hands with 2008
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; create an HTTPS listener without a policy and you get &lt;code&gt;ELBSecurityPolicy-2016-08&lt;/code&gt;, which accepts TLS 1.0 and 1.1. Also off by default on the same resource: access logs and deletion protection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; nobody notices, because modern browsers pick 1.2+. Then a customer's security team runs an external scan before signing, and the deal stalls on a finding one attribute would have prevented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;ssl_policy&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ELBSecurityPolicy-TLS13-1-2-2021-06"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And on the &lt;code&gt;aws_lb&lt;/code&gt; itself: &lt;code&gt;access_logs { enabled = true }&lt;/code&gt;, &lt;code&gt;enable_deletion_protection = true&lt;/code&gt;, and &lt;code&gt;drop_invalid_header_fields = true&lt;/code&gt; while you are there.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. VPC Flow Logs do not exist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; no VPC has flow logs until you create them. No network-level record of anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; GuardDuty flags an instance talking to a known-bad IP. The first question is "since when, and what else did it talk to?" Without flow logs your incident response runs on vibes, and "was anything exfiltrated?" gets answered conservatively, which means expensively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_flow_log"&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;traffic_type&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ALL"&lt;/span&gt;
  &lt;span class="nx"&gt;log_destination_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cloud-watch-logs"&lt;/span&gt;
  &lt;span class="nx"&gt;log_destination&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;iam_role_arn&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&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;ACCEPT&lt;/code&gt; only or &lt;code&gt;REJECT&lt;/code&gt; only is half an audit trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Services create log groups you never asked for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; log groups default to Never Expire and an AWS-managed key. The sneaky part: RDS log exports, Container Insights, and Lambda create their own log groups on first write, with exactly those defaults, outside your Terraform state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; you enable RDS log exports. RDS creates &lt;code&gt;/aws/rds/instance/.../postgresql&lt;/code&gt; for you. Three years later that orphan holds three years of connection logs plus whatever your app leaked into query errors, retained forever, under a key you did not choose, and invisible to &lt;code&gt;terraform destroy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; pre-create every log group a service will write to, before the service exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_log_group"&lt;/span&gt; &lt;span class="s2"&gt;"rds"&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="s2"&gt;"/aws/rds/instance/myapp-db/postgresql"&lt;/span&gt;
  &lt;span class="nx"&gt;retention_in_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2192&lt;/span&gt;
  &lt;span class="nx"&gt;kms_key_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_kms_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. The default security group is a party where everyone knows everyone
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; every VPC ships with a default security group that allows all traffic between members and all outbound. Anything launched without an explicit group lands in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; a contractor spins up a "quick utility box" without thinking about security groups. It joins the default one, alongside everything else that drifted in over the years. That box gets popped, and lateral movement is free, because membership is the authorization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; you cannot delete the default group, but you can strip it bare. A resource with no ingress or egress blocks removes every rule, so anything landing there can talk to precisely nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_default_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt;   &lt;span class="p"&gt;=&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="s2"&gt;"default-DO-NOT-USE"&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;Loud failure beats silent success.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. RDS backups: one day via the API, zero via Terraform
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; backup retention is one day via API or CLI, seven via the console, and &lt;code&gt;backup_retention_period&lt;/code&gt; defaults to 0 in Terraform. Zero. Automated backups off. Deletion protection is also off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; an instance defined without that line has no backups at all, and it passes plan, apply, and review, because absence does not show up in a diff. You find out during your first real restore attempt, which is the single worst moment available. Bonus: with deletion protection off, one &lt;code&gt;terraform destroy&lt;/code&gt; in the wrong workspace takes the database and its backups together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;backup_retention_period&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="nx"&gt;deletion_protection&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;skip_final_snapshot&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nx"&gt;final_snapshot_identifier&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myapp-db-final"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then AWS Backup with a locked vault on top, because backups attached to the instance share the instance's blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. EBS encryption by default is off, in every region separately
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; account-level EBS encryption by default is disabled, and it is a per-region setting. Turning it on in us-east-1 does nothing for us-west-2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; your Terraform encrypts every volume it manages. Then someone launches a console instance for a one-off migration, copies a database dump onto it, and that volume is plaintext, because the account default governs ad hoc resources, not your module.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; one resource, once per region you use, including the ones you think you do not use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ebs_encryption_by_default"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;enabled&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. SNS stores your alerts in plaintext
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; server-side encryption on SNS topics is off until you set a KMS key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; mostly as an audit finding. Occasionally worse: a team wires appointment reminders through SNS, and now message bodies with patient data sit unencrypted in the messaging layer while every database in the stack is dutifully encrypted. One-line fix or an hours-long finding memo. Pick the line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sns_topic"&lt;/span&gt; &lt;span class="s2"&gt;"alerts"&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="s2"&gt;"security-alerts"&lt;/span&gt;
  &lt;span class="nx"&gt;kms_master_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_kms_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real gotcha: once the topic is encrypted, CloudWatch and EventBridge need &lt;code&gt;kms:Decrypt&lt;/code&gt; and &lt;code&gt;kms:GenerateDataKey*&lt;/code&gt; in the key policy, not in IAM. Otherwise every alarm publish fails silently. Test the path end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. S3 got fixed, which is exactly the problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default (current):&lt;/strong&gt; since January 2023 every new object is encrypted with SSE-S3. Since April 2023 new buckets get Block Public Access on and ACLs off. Credit where due: the two most famous S3 footguns are gone for new buckets. Still off: server access logging, versioning, and any customer-managed key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; "S3 encrypts by default now" becomes the reason nobody configures anything further. A sensitive bucket ends up with no access log, no versioning, no policy denying plaintext transport, and a key nobody can audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; for a bucket that matters: SSE-KMS with your own key, versioning on, &lt;code&gt;aws_s3_bucket_logging&lt;/code&gt; to a separate log bucket, and a bucket policy denying &lt;code&gt;aws:SecureTransport = false&lt;/code&gt;. The 2023 change is your floor, not your control.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. ECS Exec gives you a shell, and records nothing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Default:&lt;/strong&gt; ECS Exec logging is &lt;code&gt;DEFAULT&lt;/code&gt;, meaning "whatever awslogs config the task has". If the container has none, session input and output are not logged at all. KMS encryption of the session channel is opt-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it bites:&lt;/strong&gt; an engineer execs into a production task to debug, runs a few queries, pastes results somewhere. Months later an access review needs that session. CloudTrail says a shell was opened at 14:32. That is the entire record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; on the cluster, &lt;code&gt;execute_command_configuration&lt;/code&gt; with &lt;code&gt;logging = "OVERRIDE"&lt;/code&gt;, a dedicated encrypted log group, and a &lt;code&gt;kms_key_id&lt;/code&gt; for the channel. Every session becomes a transcript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate it or relive it forever
&lt;/h2&gt;

&lt;p&gt;Twelve items is too many for humans to re-run reliably. That is the real lesson. What works for me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One baseline module&lt;/strong&gt; for the apply-once account controls: the trail, AWS Config rules like &lt;code&gt;RDS_STORAGE_ENCRYPTED&lt;/code&gt; and &lt;code&gt;VPC_FLOW_LOGS_ENABLED&lt;/code&gt;, EBS default encryption, the account-level S3 public access block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoded invariants&lt;/strong&gt; in workload modules. &lt;code&gt;storage_encrypted&lt;/code&gt;, &lt;code&gt;publicly_accessible = false&lt;/code&gt;, forced TLS, and log validation are not variables in mine. Exposing them as inputs just recreates the permissive default one level up, with your name on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy as code for the rest.&lt;/strong&gt; &lt;code&gt;checkov&lt;/code&gt; in CI catches regressions in code. AWS Config catches the console-created, script-created, "temporary" resources your code never met. You need both, because the resources that hurt most never saw a pull request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The pattern across all twelve is the same. AWS optimizes for the first five minutes. Your auditor cares about year five, when someone asks who accessed what and the answer has to exist. Those two goals produce opposite defaults, and closing the gap is, by AWS's own shared responsibility model, your job.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not legal or compliance advice. I am an infrastructure engineer, and defaults change, sometimes even in the right direction, so check each one against current docs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Which default bit you? I only know the twelve that bit me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>beginners</category>
      <category>devops</category>
    </item>
    <item>
      <title>Claude Code's Max Effort Cost Me 8x More. It Wrote the Same Code.</title>
      <dc:creator>vadim albarov</dc:creator>
      <pubDate>Tue, 15 Sep 2026 21:44:18 +0000</pubDate>
      <link>https://dev.to/vadim_albarov/claude-codes-max-effort-cost-me-8x-more-it-wrote-the-same-code-23hc</link>
      <guid>https://dev.to/vadim_albarov/claude-codes-max-effort-cost-me-8x-more-it-wrote-the-same-code-23hc</guid>
      <description>&lt;p&gt;Claude Code has an &lt;code&gt;/effort&lt;/code&gt; command with five levels: low, medium, high, xhigh, max. The help text for low says "quick, straightforward implementation with minimal overhead." The natural assumption is that the higher levels buy you better code, and that the price is time and tokens.&lt;/p&gt;

&lt;p&gt;I wanted to see that trade-off on a chart. So I built a small harness, gave every level the same task in a clean copy of the same folder, and measured. I did it three times with three different task shapes.&lt;/p&gt;

&lt;p&gt;The chart never appeared. Every level got the same score on every task. Only the clock and the invoice moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The harness
&lt;/h2&gt;

&lt;p&gt;Nothing clever. One headless Claude Code process per level, running in its own copy of the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;prompt.txt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--effort&lt;/span&gt; low &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; stream-json &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; acceptEdits &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Write,Edit,Bash(python*)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stream-json output goes to a log file. A small viewer prints readable progress in a visible PowerShell window, so I could watch five terminals work through the same problem one after another. When all runs finish, an analyzer parses the logs for turns, tool calls, cost, and token counts, then runs a hidden test suite against each folder. The child sessions never see the hidden tests.&lt;/p&gt;

&lt;p&gt;I ran each level once. Keep that in mind for every number below. One sample is enough to show a 9x gap. It is not enough to tell a 10 percent difference from noise.&lt;/p&gt;

&lt;p&gt;Model was Fable 5.1 on every run. Claude Code 2.1.272.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark 1: implement from docstrings
&lt;/h2&gt;

&lt;p&gt;A tiny Python order-processing library: money parsing, coupons, tax, inventory with reservations. Nine functions and one class, all stubbed out with &lt;code&gt;NotImplementedError&lt;/code&gt; and detailed docstrings. Three public tests visible. 57 hidden tests.&lt;/p&gt;

&lt;p&gt;The prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Implement every function and method marked &lt;code&gt;# TODO&lt;/code&gt; in the &lt;code&gt;shop&lt;/code&gt; package exactly according to the docstrings. Do not change the docstrings, models.py, or anything in tests/. Run &lt;code&gt;python -m pytest -q&lt;/code&gt; to check your work. When done, reply with a one-line summary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;effort&lt;/th&gt;
&lt;th&gt;turns&lt;/th&gt;
&lt;th&gt;wall time&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;th&gt;output tokens&lt;/th&gt;
&lt;th&gt;hidden tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;77 s&lt;/td&gt;
&lt;td&gt;$0.59&lt;/td&gt;
&lt;td&gt;5,174&lt;/td&gt;
&lt;td&gt;57/57&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;69 s&lt;/td&gt;
&lt;td&gt;$0.59&lt;/td&gt;
&lt;td&gt;4,995&lt;/td&gt;
&lt;td&gt;57/57&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;174 s&lt;/td&gt;
&lt;td&gt;$1.22&lt;/td&gt;
&lt;td&gt;13,009&lt;/td&gt;
&lt;td&gt;57/57&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xhigh&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;297 s&lt;/td&gt;
&lt;td&gt;$2.04&lt;/td&gt;
&lt;td&gt;24,919&lt;/td&gt;
&lt;td&gt;57/57&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;693 s&lt;/td&gt;
&lt;td&gt;$4.72&lt;/td&gt;
&lt;td&gt;62,196&lt;/td&gt;
&lt;td&gt;57/57&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every level passed everything on the first try. Max took eleven and a half minutes and $4.72 to arrive at the same 57/57 that low reached in 77 seconds for 59 cents. Output tokens grew 12x. Tool calls grew only about 3x, so most of that growth is reasoning, not doing.&lt;/p&gt;

&lt;p&gt;Low and medium were near clones of each other. Same number of turns, same sequence: read, write three files, run pytest, one sanity check, done. The diff between their outputs is 41 lines of reordering.&lt;/p&gt;

&lt;p&gt;The code was not identical across levels, though. Since the tests could not separate them, I diffed the implementations by hand.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low and medium&lt;/strong&gt; parsed money strings by stripping the sign and the dollar symbol and checking each character. They delete commas, so "1,23.45" is accepted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High&lt;/strong&gt; wrote one verbose regex with named groups that enforces comma groups of three.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Xhigh and max&lt;/strong&gt; factored parsing into helpers. Max also widened &lt;code&gt;Decimal&lt;/code&gt; precision with a &lt;code&gt;localcontext&lt;/code&gt; for very large amounts. No test covers that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tax rounding&lt;/strong&gt;: low and medium used &lt;code&gt;Decimal&lt;/code&gt; with &lt;code&gt;quantize&lt;/code&gt;. High, xhigh and max switched to &lt;code&gt;Fraction&lt;/code&gt; for exact rational math and a hand-written half-up round.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max&lt;/strong&gt; added validation nobody asked for. The inventory constructor rejects negative stock, and reserve rejects quantities below one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So higher effort produced more defensive, more factored code. It did not produce more correct code, because there was nothing left to be more correct about.&lt;/p&gt;

&lt;p&gt;One detail I liked: the only actual spec deviation came from &lt;strong&gt;high&lt;/strong&gt;, not low. The docstring said to raise &lt;code&gt;KeyError&lt;/code&gt; with the coupon code as the message. High raised it with the uppercased code. My test used an all-caps code, so it passed anyway. Effort level did not predict spec fidelity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark 2: port it to Go
&lt;/h2&gt;

&lt;p&gt;The first task was too easy. A port to another language has real traps: Python has &lt;code&gt;Decimal&lt;/code&gt;, &lt;code&gt;Fraction&lt;/code&gt;, arbitrary ints and exceptions. Go has none of those. Errors are values, half-up rounding is manual, and the model has to decide how an &lt;code&gt;OutOfStock&lt;/code&gt; error carries its fields.&lt;/p&gt;

&lt;p&gt;Same harness. The base folder held the Python reference source and an empty Go module. Hidden Go tests import a fixed package path. The analyzer also runs &lt;code&gt;go vet&lt;/code&gt; and &lt;code&gt;gofmt -l&lt;/code&gt; for free quality signals.&lt;/p&gt;

&lt;p&gt;I ran low, medium and high, then stopped to save budget.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;effort&lt;/th&gt;
&lt;th&gt;turns&lt;/th&gt;
&lt;th&gt;wall time&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;th&gt;output tokens&lt;/th&gt;
&lt;th&gt;hidden tests&lt;/th&gt;
&lt;th&gt;gofmt&lt;/th&gt;
&lt;th&gt;Go lines&lt;/th&gt;
&lt;th&gt;test lines&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;135 s&lt;/td&gt;
&lt;td&gt;$1.04&lt;/td&gt;
&lt;td&gt;11,175&lt;/td&gt;
&lt;td&gt;all pass&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;442&lt;/td&gt;
&lt;td&gt;148&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;401 s&lt;/td&gt;
&lt;td&gt;$2.60&lt;/td&gt;
&lt;td&gt;31,503&lt;/td&gt;
&lt;td&gt;all pass&lt;/td&gt;
&lt;td&gt;test file unformatted&lt;/td&gt;
&lt;td&gt;498&lt;/td&gt;
&lt;td&gt;339&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;470 s&lt;/td&gt;
&lt;td&gt;$3.44&lt;/td&gt;
&lt;td&gt;38,673&lt;/td&gt;
&lt;td&gt;all pass&lt;/td&gt;
&lt;td&gt;ok&lt;/td&gt;
&lt;td&gt;574&lt;/td&gt;
&lt;td&gt;420&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Again, flat on correctness. Cost roughly linear in effort. What each level did with its budget was different, and this part was genuinely interesting to watch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low&lt;/strong&gt; read the sources, wrote five files, ran vet and test, and stopped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium&lt;/strong&gt; wanted to probe the Python original before writing Go, to confirm exact rounding and error behavior. Good instinct. But my tool allowlist for that run did not include Python, so it retried across PowerShell and Bash several times before giving up. That is harness noise, not effort. It also wrote a test suite more than twice the size of low's, and never ran gofmt on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High&lt;/strong&gt;, with Python allowed, built its own cross-language oracle. It wrote a Python script that ran the original code over many inputs, generated a Go test file from the results, ran that against its port, and then deleted the generated files. That is exactly the grading technique I had planned to build myself. High invented it unprompted, which is the kind of behavior you would hope a higher effort setting buys.&lt;/p&gt;

&lt;p&gt;It just did not change the score.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark 3: bug hunt with no execution
&lt;/h2&gt;

&lt;p&gt;Both tasks so far had the same two properties: the spec was complete, and verification was cheap. Any level can iterate against a test suite until green. Extra thinking has nothing to buy.&lt;/p&gt;

&lt;p&gt;So I broke both. A working Go version of the library with 8 planted bugs: a wrong rounding constant, bad comma grouping, a sign error in refund splits, case-sensitive coupon codes, percentage coupons discounting gift cards, tax applied to exempt categories, a non-atomic inventory reserve, and a double-release that inflates stock. The prompt gave 8 vague user reports, one per bug, and one hard rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You cannot run any commands in this session (no go build, no tests). Reason carefully from the code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tool allowlist: Read, Grep, Glob, Edit. Nothing else.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;effort&lt;/th&gt;
&lt;th&gt;turns&lt;/th&gt;
&lt;th&gt;wall time&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;th&gt;output tokens&lt;/th&gt;
&lt;th&gt;bugs fixed&lt;/th&gt;
&lt;th&gt;diff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;52 s&lt;/td&gt;
&lt;td&gt;$0.94&lt;/td&gt;
&lt;td&gt;3,677&lt;/td&gt;
&lt;td&gt;7/8&lt;/td&gt;
&lt;td&gt;+20 / -5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;55 s&lt;/td&gt;
&lt;td&gt;$0.86&lt;/td&gt;
&lt;td&gt;3,585&lt;/td&gt;
&lt;td&gt;7/8&lt;/td&gt;
&lt;td&gt;+16 / -3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;69 s&lt;/td&gt;
&lt;td&gt;$0.94&lt;/td&gt;
&lt;td&gt;5,055&lt;/td&gt;
&lt;td&gt;7/8&lt;/td&gt;
&lt;td&gt;+26 / -4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xhigh&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;125 s&lt;/td&gt;
&lt;td&gt;$1.30&lt;/td&gt;
&lt;td&gt;10,200&lt;/td&gt;
&lt;td&gt;7/8&lt;/td&gt;
&lt;td&gt;+21 / -4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Finally a miss. All four levels missed the same bug, the tax one, and all four made the identical wrong fix. When I looked closely, that was my fault.&lt;/p&gt;

&lt;p&gt;The correct rule was proportional: remove the exempt fraction of the pre-discount subtotal from the post-discount taxable amount. That rule lived only in the Python docstring, which was not in the Go project. From the Go code and the report "grocery and book orders are being taxed," subtracting the exempt amount directly is a completely reasonable fix. Four independent runs converging on it is evidence that the intended rule was not inferable from what they were given. It is not evidence that effort failed.&lt;/p&gt;

&lt;p&gt;So the fair score is 7 of 7 for every level. Flat again.&lt;/p&gt;

&lt;p&gt;What effort did change here was, once more, output tokens: 3.6k, 3.6k, 5k, 10k. Wall time and cost followed. And scope creep showed up at low, high and xhigh, each of which invented a fuzzy gift-card category matcher that handles "Gift Card", "gift_card" and "GIFT-CARD" when the original code used an exact string. Medium alone made the exact minimal change the prompt asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Effort controls how much the model deliberates, not what it knows.&lt;/strong&gt; On every task here the answer was fully determined and reachable in one careful pass. Given that, low found it. Max found it too, after thinking about it for eleven minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification loops make effort irrelevant.&lt;/strong&gt; If the session can run tests, any level will iterate until green. The Python and Go benchmarks measured persistence, and persistence was free at every level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Higher effort spends its budget on self-checks and defensive code.&lt;/strong&gt; Oracles, throwaway verification scripts, extra validation, regexes that reject malformed input the spec never mentioned. Sometimes that is exactly what you want. In a codebase with a scope rule, it is unrequested behavior you now have to review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium was the quiet winner on discipline.&lt;/strong&gt; It never won on speed or cost, but on the bug hunt it was the only level that did precisely what the prompt asked and nothing more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The total bill for this experiment was about $20.&lt;/strong&gt; Roughly $9 for the Python runs, $7 for the Go runs, $4 for the bug hunt. The max Python run alone was almost a quarter of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where effort probably should matter
&lt;/h2&gt;

&lt;p&gt;I have not proven that effort is useless. I have shown three tasks where it did not help, and I think I know why. Effort should matter when deliberation itself is the bottleneck:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two-step inference.&lt;/strong&gt; The symptom is in one file and the cause is in another, and the code near the symptom looks fine. A fix at the symptom passes the reported case and fails hidden siblings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A decoy fix that breaks a hidden invariant.&lt;/strong&gt; An inventory patch that resolves the report and introduces a race under &lt;code&gt;go test -race&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A constraint hidden in a README that conflicts with the obvious fix.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A codebase too large to read.&lt;/strong&gt; Around 2,000 lines, where effort shows in search strategy and in what gets verified before editing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat runs.&lt;/strong&gt; Three samples per level at minimum. My single-sample wall times have variance comparable to the gaps between adjacent levels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the next benchmark. Until then, my working rule: for a well-specified implementation task where the session can run tests, low or medium. Reserve high and above for tasks where the model cannot check itself and the answer is not sitting in the docstring.&lt;/p&gt;

&lt;p&gt;And watch the clock. Nothing on max was wrong. It just took nine times longer to be right.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>llm</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
