<?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: Saurav Niraula</title>
    <description>The latest articles on DEV Community by Saurav Niraula (@sauravniraula).</description>
    <link>https://dev.to/sauravniraula</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%2F3434257%2F14aa6473-3981-4f18-8e2d-7e646613dcbd.png</url>
      <title>DEV Community: Saurav Niraula</title>
      <link>https://dev.to/sauravniraula</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sauravniraula"/>
    <language>en</language>
    <item>
      <title>When the agent writes the patch, who audits the tests?</title>
      <dc:creator>Saurav Niraula</dc:creator>
      <pubDate>Sun, 15 Nov 2026 18:12:35 +0000</pubDate>
      <link>https://dev.to/sauravniraula/when-the-agent-writes-the-patch-who-audits-the-tests-25k5</link>
      <guid>https://dev.to/sauravniraula/when-the-agent-writes-the-patch-who-audits-the-tests-25k5</guid>
      <description>&lt;p&gt;In the METR randomized trial published in July 2025, sixteen experienced open-source developers worked on 246 real issues in their own repositories. Half the tasks allowed AI tools, half did not. The AI-allowed tasks took 19% longer. Before the study the same developers expected a 24% speedup, and after living through the slowdown they still believed they had been sped up by 20%.&lt;/p&gt;

&lt;p&gt;That gap between what happened and what the developers felt is the most useful result in the paper, and it is not really about coding speed. It is about measurement. People are bad at knowing whether a tool helped, and the tool is now producing the thing that tells them.&lt;/p&gt;

&lt;p&gt;Apply that to tests and you get the quiet failure nobody is writing postmortems about yet. The agent writes the code. The agent writes the tests. The agent runs the suite and reports green. Every step is plausible, the diff is clean, and the review takes four minutes because the checks passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ways a test suite gets laundered
&lt;/h2&gt;

&lt;p&gt;None of these require an adversarial model. They are the shortcuts a model takes when the goal is "make the suite pass," which is the goal you wrote in the prompt.&lt;/p&gt;

&lt;p&gt;A new test that asserts the code's current behavior instead of the intended behavior. It passes on the first run, it has a nice name, and it would pass if the function returned zero for everything.&lt;/p&gt;

&lt;p&gt;A snapshot that got regenerated. &lt;code&gt;-u&lt;/code&gt; is one keystroke away and the resulting diff is unreviewable by construction, which is the whole reason it slips through.&lt;/p&gt;

&lt;p&gt;A broad &lt;code&gt;except&lt;/code&gt; or a &lt;code&gt;try/except: pass&lt;/code&gt; inserted in the module, converting a real failure into silence that the test reads as success.&lt;/p&gt;

&lt;p&gt;An assertion loosened from equality to truthiness. &lt;code&gt;assertEqual(total, 41.50)&lt;/code&gt; becomes &lt;code&gt;assertIsNotNone(total)&lt;/code&gt;. Both are green. Only one is a test.&lt;/p&gt;

&lt;p&gt;A deleted case, or a skipped one with a TODO. The count of tests stays roughly stable so nobody notices.&lt;/p&gt;

&lt;p&gt;A timeout raised from 2s to 30s, which hides a performance regression behind a green check.&lt;/p&gt;

&lt;p&gt;Every one of these shows up in the diff as a small, defensible line. That is what makes it survive review: nothing looks wrong in isolation, and reviewers anchor on the code file being the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage is not the control you think it is
&lt;/h2&gt;

&lt;p&gt;Coverage measures whether a line executed, not whether anything was verified. A test that calls a function and asserts nothing gives you 100% coverage of that function and zero protection. An agent optimizing for coverage will produce exactly that, because it is the cheapest way to satisfy the metric you put in the prompt.&lt;/p&gt;

&lt;p&gt;The metric is the problem. If you tell an agent "increase coverage to 85%," you have defined success as execution, and the model will deliver execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutation testing on the changed lines
&lt;/h2&gt;

&lt;p&gt;The cheapest real signal I have found is mutation testing scoped to the diff. Take the changed lines, mutate them (flip a comparison, change a constant by one, negate a boolean, remove a return), and check whether the test suite notices. If a mutant survives, the tests around that line do not actually verify it.&lt;/p&gt;

&lt;p&gt;Running mutation testing on a whole repository is too slow to put in CI. Running it on the diff is not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Python: mutate only the files this PR touched&lt;/span&gt;
git diff --name-only origin/main...HEAD -- &lt;span class="s1"&gt;'*.py'&lt;/span&gt; \
  &lt;span class="o"&gt;|&lt;/span&gt; xargs mutmut run --paths-to-mutate &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; --tests-dir tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stryker does the same for JavaScript with &lt;code&gt;--mutate&lt;/code&gt; limited to changed files, PIT with &lt;code&gt;--targetClasses&lt;/code&gt; for Java. The report is a number: what fraction of injected faults did the suite catch. A patch with 40% mutation score is a patch with tests that exist and do not verify.&lt;/p&gt;

&lt;p&gt;This catches the laundered test directly, because a test that asserts nothing kills no mutants. It also catches the deleted case and the loosened assertion, which coverage never will.&lt;/p&gt;

&lt;h2&gt;
  
  
  One piece of evidence to require
&lt;/h2&gt;

&lt;p&gt;Every AI-assisted pull request should carry a failing-before proof for the behavior it claims to fix.&lt;/p&gt;

&lt;p&gt;The mechanics are simple: run the new test against the code from before the patch, and attach the failure. In git terms, stash the source change, keep the test, run it, capture the output. Then unstash and run it again. The artifact is a diff of two test runs, and it takes the model four seconds to produce if you ask for it in the same session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git stash push -- src/            &lt;span class="c"&gt;# source change only&lt;/span&gt;
npm &lt;span class="nb"&gt;test&lt;/span&gt; -- --testPathPattern changed.behavior
git stash pop
npm &lt;span class="nb"&gt;test&lt;/span&gt; -- --testPathPattern changed.behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the test passes both times, it does not test the change. Whatever else is true about the patch, the verification is decorative. That single check invalidates most of the laundered-test patterns in one step, and unlike a coverage threshold it cannot be satisfied by writing more nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the two capabilities
&lt;/h2&gt;

&lt;p&gt;Here is the part that is an architecture decision rather than a process one. The agent should not be able to edit tests and source in the same commit with the same approvals.&lt;/p&gt;

&lt;p&gt;Split them. A code change lands with tests unchanged, or a test change lands with code unchanged and a reviewer who is explicitly looking at it. &lt;code&gt;CODEOWNERS&lt;/code&gt; on the tests directory is the mechanism, and "tests modified in this PR" as a required label is the signal. It is a five-line policy that makes the failure mode visible instead of silently green.&lt;/p&gt;

&lt;p&gt;The agent can still propose test changes. It just cannot be the only witness to its own work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest version of the productivity story
&lt;/h2&gt;

&lt;p&gt;METR's follow-up work in early 2026 suggests developer experience has improved since that first trial, with an estimated speedup among returning participants, while also warning that selection effects make the number unreliable. I believe the newer estimate is closer to reality than the old one. I also think the perception gap is still there, and it matters more now that the tool writes the tests as well as the code.&lt;/p&gt;

&lt;p&gt;You cannot feel your way to "the tests verify the change." You can measure it. Mutation score on the diff, a failing-before artifact, and test files that a different person signed off on. Three checks, none of them expensive, all of them boring, which is what a control is supposed to look like.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>programming</category>
      <category>codequality</category>
    </item>
    <item>
      <title>"Zero upload" is a claim. Show me the network tab.</title>
      <dc:creator>Saurav Niraula</dc:creator>
      <pubDate>Sun, 15 Nov 2026 18:10:59 +0000</pubDate>
      <link>https://dev.to/sauravniraula/zero-upload-is-a-claim-show-me-the-network-tab-4a7k</link>
      <guid>https://dev.to/sauravniraula/zero-upload-is-a-claim-show-me-the-network-tab-4a7k</guid>
      <description>&lt;p&gt;A PDF merge tool went around my team last month. The headline was "your files never leave your device." I opened devtools out of habit. The page loaded a Google font, a session recorder, and a Sentry bundle, and it POSTed a &lt;code&gt;page_view&lt;/code&gt; with the document title in the payload. The PDF itself never left. The filename, the page count, and the fact that I was merging a contract at 11:40 p.m. did.&lt;/p&gt;

&lt;p&gt;The tool was not lying, exactly. It merged the files locally, which is a real engineering achievement and the reason I use these things. But "zero upload" is a security claim, and a security claim is worth what its enforcement is worth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture is right, and that's the frustrating part
&lt;/h2&gt;

&lt;p&gt;Moving file processing into the browser is one of the better ideas to come out of the last few years. PDF parsing via a WASM build of pdfium or qpdf, image resizing on a canvas, CSV work in plain JS, zip handling with a streaming library. No server means no retention policy, no data processing agreement, no S3 bill, no bucket that someone forgets to make private. For a 12 MB PDF, the whole job fits in the tab.&lt;/p&gt;

&lt;p&gt;Then the marketing outruns the engineering. "Bank-grade privacy" appears next to a page that calls four third parties before the first paint.&lt;/p&gt;

&lt;p&gt;The gap is always the same three things: analytics, error reporting, and fonts. Sentry is the worst of them because its payloads are designed to carry context. File names, breadcrumbs, and the last network request can all end up in an event, and nobody reads the scrubbing config until a customer asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the claim enforceable
&lt;/h2&gt;

&lt;p&gt;If you want to say the files never leave, put it in a header where it is checkable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nc"&gt;Security&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nc"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="s1"&gt;'wasm-unsafe-eval'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ancestors&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;connect-src 'none'&lt;/code&gt; blocks fetch, XHR, WebSocket, EventSource, and &lt;code&gt;navigator.sendBeacon&lt;/code&gt;. That is the actual guarantee. Not the copy, not the privacy policy, the header. &lt;code&gt;'wasm-unsafe-eval'&lt;/code&gt; is there because compiling a WASM module is blocked by a strict &lt;code&gt;script-src&lt;/code&gt; without it, which is a detail people discover at 2 a.m. the night before launch.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;form-action 'none'&lt;/code&gt; and &lt;code&gt;base-uri 'none'&lt;/code&gt; close the two dumb paths around &lt;code&gt;connect-src&lt;/code&gt;. And if you truly ship no telemetry, put a comment in the repo explaining that the header is a product promise, so the next person who adds a chat widget gets a failing test instead of a silent policy change.&lt;/p&gt;

&lt;p&gt;If you cannot set &lt;code&gt;connect-src 'none'&lt;/code&gt; because you need a support chat and a payment SDK, that is fine. Say "processed locally, no files uploaded" instead. That sentence is true and defensible. The other one is a promise your own CDN can break.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints nobody puts in the demo
&lt;/h2&gt;

&lt;p&gt;The privacy story gets all the attention. The engineering is harder than the landing page suggests, and the limits are predictable:&lt;/p&gt;

&lt;p&gt;Memory. WASM32 gives you a 4 GB address space in theory and browsers give you far less in practice, especially on mobile, where the tab gets killed without a warning you can catch. A 300 MB scanned PDF will not fit if you decode every page at once. You need to process page by page, write intermediate results to OPFS, and free aggressively. If your demo file is 200 KB, you have not tested the product.&lt;/p&gt;

&lt;p&gt;Streaming. &lt;code&gt;File.stream()&lt;/code&gt; gives you a &lt;code&gt;ReadableStream&lt;/code&gt;, and &lt;code&gt;TransformStream&lt;/code&gt; lets you pass chunks to a worker without materializing the whole file. That works beautifully for line-oriented formats, hashing pipelines, and archives. It does not work for PDFs, because PDF structure is random access by design, the xref table can live at the end of the file, and incremental parsing gets ugly fast. Know which case you are in before you promise streaming.&lt;/p&gt;

&lt;p&gt;Hashing. WebCrypto's &lt;code&gt;crypto.subtle.digest&lt;/code&gt; takes a full buffer, which defeats the point of streaming a 2 GB file. If you need an incremental SHA-256 you will end up pulling in a WASM implementation or a pure-JS one that is slow enough to notice. I have lost an afternoon to this more than once.&lt;/p&gt;

&lt;p&gt;Jank. Parsing 400 pages on the main thread freezes the UI and Safari will helpfully kill the page for you. Worker plus &lt;code&gt;postMessage&lt;/code&gt; with transferable &lt;code&gt;ArrayBuffer&lt;/code&gt;s, always. If your worker copies instead of transfers, you will watch memory double for no reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd want to see before trusting one
&lt;/h2&gt;

&lt;p&gt;A page that merges my files locally and proves it. Three things make that checkable in under a minute: the CSP header, a network tab that stays empty while the file processes, and source you can read or a build you can diff. If the tool is closed source, the header plus an empty network panel is still most of the way there, and it is far more than the privacy policy tells you.&lt;/p&gt;

&lt;p&gt;None of this is an argument against the architecture. It is an argument against describing a local-first tool with the vocabulary of a security product while shipping the telemetry of an ad-supported one.&lt;/p&gt;

&lt;p&gt;The PDF merge tool from my team's Slack got the second half right. The files stayed on the machine. The metadata did not, and nobody had written down which one the promise covered.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webassembly</category>
      <category>privacy</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your cron job will fire twice. Design for it.</title>
      <dc:creator>Saurav Niraula</dc:creator>
      <pubDate>Sun, 15 Nov 2026 18:09:18 +0000</pubDate>
      <link>https://dev.to/sauravniraula/your-cron-job-will-fire-twice-design-for-it-4e81</link>
      <guid>https://dev.to/sauravniraula/your-cron-job-will-fire-twice-design-for-it-4e81</guid>
      <description>&lt;p&gt;Every tutorial for scheduled work has the same shape. A cron expression, a route handler, a loop over users, a call to an email API. It runs on the author's machine for two days and then it ships.&lt;/p&gt;

&lt;p&gt;Then production happens. The platform retries a 502 that actually succeeded. A deploy lands mid-run and two workers process the same batch. The retry after a rate limit sends the digest a second time. A customer forwards the duplicate to support, and someone writes a ticket titled "we got this twice," and that is the first time anybody thinks about delivery semantics.&lt;/p&gt;

&lt;p&gt;Cron is a trigger, not a scheduler. The moment your work leaves the process, you are in at-least-once territory, and at-least-once means duplicates are not an edge case. They are the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the duplicate impossible, not unlikely
&lt;/h2&gt;

&lt;p&gt;The fix is not a &lt;code&gt;sent_at&lt;/code&gt; column you check before sending, because check-then-send has a race and you will find it at 3 a.m. with two workers that both read &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix is a unique constraint that decides the winner atomically, before any side effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"pg"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;rowCount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sb"&gt;`insert into processed_events (event_id, claimed_at)
     values ($1, now())
     on conflict (event_id) do nothing`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// true only for the first caller&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// someone else has it, or we already did&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter more than the code. The key must come from the event, not from a timestamp or a random UUID you generate at send time, and it must be stable across retries. If your queue redelivers with a fresh ID, the constraint does nothing for you. And &lt;code&gt;on conflict do nothing&lt;/code&gt; returning &lt;code&gt;rowCount = 0&lt;/code&gt; tells you the event was already claimed, which is exactly the information you need to skip quietly instead of erroring.&lt;/p&gt;

&lt;p&gt;For the recurring job itself, the same idea applies at the job level. A single-flight lock beats a &lt;code&gt;if (running) return&lt;/code&gt; flag in memory, which does nothing when you run three replicas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="sb"&gt;`select pg_try_advisory_lock(hashtext('nightly-digest')) as locked`&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;rows&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="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runDigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`select pg_advisory_unlock(hashtext('nightly-digest'))`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bound the concurrency or drown
&lt;/h2&gt;

&lt;p&gt;The other half of this problem is what happens when your provider pushes back. A loop over ten thousand users with &lt;code&gt;await&lt;/code&gt; inside is slow but polite. A loop with &lt;code&gt;Promise.all&lt;/code&gt; is fast and gets you a 429 within seconds, and then the retry storm makes it worse.&lt;/p&gt;

&lt;p&gt;Bound the in-flight requests, and treat 429 as an instruction rather than an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pLimit&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"p-limit"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// provider allows ~5 concurrent, leave headroom&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;limit&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/v1/messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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="s2"&gt;"retry-after"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&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;250&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// jitter&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sendWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in there that get skipped in tutorials. &lt;code&gt;Retry-After&lt;/code&gt; is honored when the server sends it, because the server knows better than your exponential curve. Jitter is added, because identical backoff across workers synchronizes them into a thundering herd. And there is an attempt cap, because a permanent failure should land in a dead letter queue where a human sees it, not in an infinite loop that burns your quota.&lt;/p&gt;

&lt;p&gt;Cap it, then route it. A row in a &lt;code&gt;failed_jobs&lt;/code&gt; table with the last error and the payload hash is worth more than any amount of clever retry logic, because it makes the failure visible instead of ambient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't enqueue from inside the transaction you can still roll back
&lt;/h2&gt;

&lt;p&gt;One pattern bites every team once. You write to the database and publish to the queue in the same handler. The write commits, the publish fails, and the work is lost. Or the write rolls back after the publish, and the consumer acts on a record that does not exist.&lt;/p&gt;

&lt;p&gt;Use an outbox: write the event to a table in the same transaction as the state change, then have a separate poller publish and mark it sent. It costs one table and one query per tick, and it converts a distributed consistency problem into a local one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="kt"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;topic&lt;/span&gt;       &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;     &lt;span class="kt"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sent_at&lt;/span&gt;     &lt;span class="kt"&gt;timestamptz&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;sent_at&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The poller claims rows with &lt;code&gt;for update skip locked&lt;/code&gt;, which lets you run it on every replica without coordination. That is the whole trick, and it is the same trick as the idempotency key: let the database decide who wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test that would have caught it
&lt;/h2&gt;

&lt;p&gt;Before any of this, write the test that fails on the tutorial version of the code.&lt;/p&gt;

&lt;p&gt;Fire the same event twice into your handler with the same event ID. Assert that exactly one email exists in the fake provider, that the second call returns 2xx, and that no error is thrown. Then add the harder one: two concurrent calls with the same ID, awaited together, asserting one send. That second test is the one that proves your constraint is doing the work instead of your application logic.&lt;/p&gt;

&lt;p&gt;Both run in about fifty milliseconds. Neither requires a queue, a fake clock, or a staging environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually owe the reader
&lt;/h2&gt;

&lt;p&gt;The "cron plus webhook" post is not wrong because it is simple. It is wrong because it teaches a delivery model that does not exist. Nobody gets exactly-once across a network boundary, and the tutorials that skip that sentence are the reason the same digest goes out twice to a customer who has already unsubscribed once.&lt;/p&gt;

&lt;p&gt;Write the duplicate test first. Put the unique index in before the feature ships. And when someone asks why there is a &lt;code&gt;processed_events&lt;/code&gt; table that seems to do nothing, tell them it is the part of the system that keeps the rest of it honest.&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>Your AGENTS.md is code. You just never reviewed it.</title>
      <dc:creator>Saurav Niraula</dc:creator>
      <pubDate>Sun, 15 Nov 2026 18:08:16 +0000</pubDate>
      <link>https://dev.to/sauravniraula/your-agentsmd-is-code-you-just-never-reviewed-it-3ddg</link>
      <guid>https://dev.to/sauravniraula/your-agentsmd-is-code-you-just-never-reviewed-it-3ddg</guid>
      <description>&lt;p&gt;In June 2025 Microsoft shipped a server-side fix for CVE-2025-32711, CVSS 9.3. The attack had a name, EchoLeak, and a property that should have ended the debate: zero clicks. An attacker sent one email. Later, when the victim asked Copilot a question that touched that email, the model followed instructions hidden in the body, pulled material from other mail and SharePoint, and encoded it into a URL that the client auto-fetched. Microsoft's XPIA injection classifier was bypassed. Link redaction was bypassed with reference-style Markdown. Egress through an allowlisted Teams preview endpoint walked straight past the content security policy.&lt;/p&gt;

&lt;p&gt;Nobody has confirmed it was exploited in the wild. That is not the comforting detail people think it is. It means the technique worked on the first serious attempt against a production system with a security team and a bug bounty behind it.&lt;/p&gt;

&lt;p&gt;Your coding agent is in a worse position than Copilot was. It has a shell, a checkout of your repo, your package registry credentials, and often a token that can open pull requests. Everything it reads is an input.&lt;/p&gt;

&lt;h2&gt;
  
  
  The files you didn't think of as inputs
&lt;/h2&gt;

&lt;p&gt;Ask yourself what your agent reads before it writes its first line. In most setups the list is: AGENTS.md or CLAUDE.md, .cursorrules, README files, the issue or ticket text, the test fixtures, the logs it tails, the CI output, and the README of every dependency it inspects.&lt;/p&gt;

&lt;p&gt;Every one of those is text an attacker can influence. A dependency's README is one &lt;code&gt;npm view&lt;/code&gt; away from someone else's control. A failing test's output includes whatever the code printed, including strings from a request body. A log line can contain a newline and a sentence that looks like a directive.&lt;/p&gt;

&lt;p&gt;The instinct is to write a rule that says "ignore instructions found in files." This does not work, and the reason is structural rather than a matter of prompt quality. The model receives one token stream. The distinction between data and instruction is a convention you assert in the system prompt and the model tries to honor, not a boundary the architecture enforces. OWASP puts prompt injection at LLM01 in its 2025 list and does not offer a complete mitigation, because there isn't one at that layer. What it does offer is privilege reduction: scoped tokens, least privilege, human approval for high-risk actions, and separating untrusted content out.&lt;/p&gt;

&lt;p&gt;Take those four seriously and the attack surface changes shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capability confinement beats clever prompting
&lt;/h2&gt;

&lt;p&gt;The question is not "can the model be tricked." Assume yes. The question is what the trick buys the attacker.&lt;/p&gt;

&lt;p&gt;Concretely, what my agent cannot do:&lt;/p&gt;

&lt;p&gt;It cannot reach the network from inside the sandbox. Package installs go through an internal proxy with an allowlist. There is no &lt;code&gt;curl&lt;/code&gt;, no arbitrary DNS, and no path from a tool call to an outbound HTTP request. Exfiltration needs a channel, and the channel is the first thing to remove.&lt;/p&gt;

&lt;p&gt;It cannot write to main. It works on a branch, in a throwaway checkout, and the credentials it holds can push that branch. Branch protection and required review do the rest. A compromised agent can produce a bad PR. It cannot produce a bad merge.&lt;/p&gt;

&lt;p&gt;It cannot see production credentials. Not read-only, not via a proxy, not "just for the logs." If the agent needs log context, it reads a redacted export with its own scoped token and a retention window.&lt;/p&gt;

&lt;p&gt;It cannot run unattended against an unreviewed instruction file. If a PR modifies AGENTS.md, CLAUDE.md, .cursorrules, or anything under .github/, the CI job flags it and requires a code owner review. This is the same treatment you give a Dockerfile, for the same reason: it is code that runs with your privileges.&lt;/p&gt;

&lt;p&gt;That last check is about ten lines of shell and it catches the laziest version of the attack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# fail the build if a PR touches agent instruction surfaces&lt;/span&gt;
git diff --name-only &lt;span class="s2"&gt;"origin/main...HEAD"&lt;/span&gt; \
  &lt;span class="o"&gt;|&lt;/span&gt; grep -Eq &lt;span class="s1"&gt;'^(AGENTS\.md|CLAUDE\.md|\.cursorrules|\.github/.*|scripts/agent/.*)$'&lt;/span&gt; \
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"agent-config change: needs code owner review"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a complete defense. It is a speed bump on the path of least resistance, and speed bumps are what supply chain security actually consists of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log the reasoning, not just the call
&lt;/h2&gt;

&lt;p&gt;The thing that made EchoLeak interesting to me was the provenance question. The model's answer looked like a normal answer. The malicious part was the URL it constructed, and the only way to notice was to know which input sentence produced it.&lt;/p&gt;

&lt;p&gt;Tool call logs should carry the justification: which file, which line, which retrieval hit. When an agent runs &lt;code&gt;rm -rf build&lt;/code&gt; at 4 a.m. because a file said to, the log should say &lt;code&gt;justified_by: notes/SETUP.md:41&lt;/code&gt;. That turns an unexplained action into a reviewable one, and it is the cheapest forensic control you can add.&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;tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;justification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;redact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"justified_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{justification['path']}:{justification['line']}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;False&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 fields. &lt;code&gt;justified_by&lt;/code&gt; and the fact that &lt;code&gt;approved&lt;/code&gt; starts false. Anything that mutates state flips that flag only through the gate you built on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The supply chain point
&lt;/h2&gt;

&lt;p&gt;We spent a decade learning that a dependency can execute arbitrary code at install time, and we built lockfiles, provenance attestations, and allowlists for it. Agent instruction files are a dependency with no lockfile and no registry. They arrive through pull requests from strangers, they change without a version bump, and they execute with more privilege than most of your npm tree.&lt;/p&gt;

&lt;p&gt;If you take one thing from the last six months of these disclosures, make it this: the model reading your workspace is executing instructions, not reading documentation. Review the instructions. Then take away the permissions you would not give a new hire on their first day.&lt;/p&gt;




&lt;p&gt;References: CVE-2025-32711 (EchoLeak), disclosed by Aim Security, patched by Microsoft in June 2025. OWASP Top 10 for LLM Applications 2025, LLM01 Prompt Injection. Analysis in arXiv:2509.10540.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>Human-in-the-loop at 3 a.m. is a rubber stamp</title>
      <dc:creator>Saurav Niraula</dc:creator>
      <pubDate>Sun, 15 Nov 2026 18:08:05 +0000</pubDate>
      <link>https://dev.to/sauravniraula/human-in-the-loop-at-3-am-is-a-rubber-stamp-gaa</link>
      <guid>https://dev.to/sauravniraula/human-in-the-loop-at-3-am-is-a-rubber-stamp-gaa</guid>
      <description>&lt;p&gt;03:12. A message in the incident channel: "Agent wants to roll back payments-api to v2.13. Approve?"&lt;/p&gt;

&lt;p&gt;A thumbs up emoji. Two seconds. The rollback was wrong, and it took another forty minutes to find out, because the approver was half awake and had no idea what the agent had actually done.&lt;/p&gt;

&lt;p&gt;Everyone building incident agents this month is adding exactly this feature: a human approval step before the agent touches production. Sixty-eight posts on DEV in the last week alone, most of them integrations with some chat product. It is the control OWASP recommends for high-risk agent actions, and it is the right instinct. The implementation is usually theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gate is a decision, not a click
&lt;/h2&gt;

&lt;p&gt;An approval is a security control only if the person clicking can answer four questions in under thirty seconds:&lt;/p&gt;

&lt;p&gt;What exactly will change? Not "roll back the service." The plan: image tag, replica count, migration state, and whether anything is being reverted that cannot be re-applied.&lt;/p&gt;

&lt;p&gt;What is the blast radius? Which services, which customer tiers, how many rows, whether the previous attempt at this is in the incident timeline.&lt;/p&gt;

&lt;p&gt;Can we undo it? Reversibility, and the time it takes to reverse. A rollback across a schema migration is not the same action as a rollback of application code, and the prompt should not look identical for both.&lt;/p&gt;

&lt;p&gt;Is this the plan that was reviewed, or a slightly different one? Hash it.&lt;/p&gt;

&lt;p&gt;If the message in Slack contains none of those, you did not build a human gate. You built a notification with a button, and you gave the agent a path to production that costs one emoji.&lt;/p&gt;

&lt;h2&gt;
  
  
  The change envelope
&lt;/h2&gt;

&lt;p&gt;I make the agent emit a fixed structure before it is allowed to ask for anything. If the structure is incomplete, the approval request does not render.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"chg_01J8Z6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"requested_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"agent/incident-crew"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"payments-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"rollback"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"from_image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"payments-api@sha256:9f2c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"to_image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"payments-api@sha256:41ab..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"commands"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kubectl -n prod set image deploy/payments-api api=...@sha256:41ab..."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"migrations_in_scope"&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="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"blast_radius"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"payments-api"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"customer_tiers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"all"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"est_requests_affected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4100&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"reversible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"reverse_plan_eta_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"INC-2291"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"deploy/4412"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"plan_sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"b7d1e0..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-16T03:22:00Z"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four fields do most of the work: &lt;code&gt;plan_sha256&lt;/code&gt; (the thing you approve is the thing that runs), &lt;code&gt;reversible&lt;/code&gt; plus the reversal ETA, &lt;code&gt;blast_radius&lt;/code&gt;, and &lt;code&gt;expires_at&lt;/code&gt;. That last one matters more than it looks. An approval from 03:12 must not still be valid at 09:40 when the on-call has gone to bed and a different incident is running.&lt;/p&gt;

&lt;p&gt;The executor verifies the hash before it applies anything. If the plan changed after the approval, the approval is void. Without that check, your "human approved" audit trail is a claim about an intention, not about an action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the gate or it isn't one
&lt;/h2&gt;

&lt;p&gt;Two numbers, pulled from the approval log, will tell you whether you have a control or a formality.&lt;/p&gt;

&lt;p&gt;Median time from request to approval. If it is under ten seconds, the reviewer is pattern-matching the name of the service, not reading the plan. That is fine for a canary bump and not fine for anything else, and the difference should be visible in the data rather than in your architecture diagram.&lt;/p&gt;

&lt;p&gt;Reversal rate within twenty-four hours. How often an approved agent change gets undone by a human shortly after. A high number means the gate is approving things it should be rejecting, and the agent's plans are worse than its confidence suggests.&lt;/p&gt;

&lt;p&gt;I also log the approver's identity and the incident they were paged for. When a postmortem says "the agent rolled back the wrong service and a human approved it," the useful question is not why the agent was wrong. It is why a tired person with three other tabs open was asked to be the last line of defense for an irreversible action at 3 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  Default reversible, gate the rest
&lt;/h2&gt;

&lt;p&gt;The better design inverts the usual instinct. Instead of gating everything and hoping the reviewer is sharp, make almost everything reversible and stop gating it:&lt;/p&gt;

&lt;p&gt;Ship behind a flag with a kill switch. Canary at 5% with an automatic abort on error-rate burn. Cap the blast radius in the mechanism, not in the prompt: quota limits, a ten-minute change window, a replica ceiling. Then the agent can move fast without asking, because being wrong is cheap and self-limiting.&lt;/p&gt;

&lt;p&gt;Keep the human gate for the short list that cannot be undone: schema migrations with data loss, deleting a bucket, rotating a credential that other services cache, force-pushing a branch, anything that touches billing records. That list is usually five items long. Write it down. Make the agent prove which one it is in.&lt;/p&gt;

&lt;p&gt;That is a much smaller promise than "human in the loop," and it is the only version I have seen hold up under a real incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part people don't want to hear
&lt;/h2&gt;

&lt;p&gt;"Human in the loop" reads like a safety property. In practice it is a staffing assumption. Every approval queue needs someone awake, competent, and holding enough context to say no. If your incident rotation is three engineers and your agents file twenty change requests a night, you have not added a control. You have added a queue with a latency SLO nobody wrote down.&lt;/p&gt;

&lt;p&gt;The gate should be a circuit breaker with a person on the other side of it, not a poll. Build the breaker first. Then decide which changes are expensive enough to wake someone up for, and be honest in the postmortem when the answer was "none of them, we just liked the feeling of approving."&lt;/p&gt;

&lt;p&gt;One more thing worth saying plainly: if your approval message is a chat bubble with a service name in it, an attacker with write access to that channel, or to the agent's own context, can produce a plausible request. The envelope does not stop that. It just makes the request falsifiable, which is the most a prompt was ever going to do.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>security</category>
      <category>sre</category>
    </item>
    <item>
      <title>Your on-call agent doesn't need memory. It needs receipts.</title>
      <dc:creator>Saurav Niraula</dc:creator>
      <pubDate>Sun, 15 Nov 2026 18:07:43 +0000</pubDate>
      <link>https://dev.to/sauravniraula/your-on-call-agent-doesnt-need-memory-it-needs-receipts-1pd3</link>
      <guid>https://dev.to/sauravniraula/your-on-call-agent-doesnt-need-memory-it-needs-receipts-1pd3</guid>
      <description>&lt;p&gt;At 02:47 my agent said: "We already tried increasing the pool size. It didn't help."&lt;/p&gt;

&lt;p&gt;I believed it for about ninety seconds. Then I checked. Nobody had tried that. Someone had suggested it in a thread six weeks earlier, and the agent had filed the suggestion away as a fact, then handed it back to me as institutional knowledge.&lt;/p&gt;

&lt;p&gt;That is the failure mode nobody demos.&lt;/p&gt;

&lt;p&gt;Sixty-nine posts about on-call agents with memory landed on DEV in the last week and a surprising number of them share a title. I read a stack. The architecture is usually the same: embed the postmortems, retrieve the top five chunks, paste them into the prompt, call it hindsight. It works in the demo because the demo has three documents and no history of being wrong.&lt;/p&gt;

&lt;p&gt;In production you get a retrieval system that cannot tell the difference between something that happened and something someone once said. That is not memory. That is a rumor mill with embeddings, and it is worse than a blank slate, because a blank slate doesn't get cited.&lt;/p&gt;

&lt;h2&gt;
  
  
  What memory actually requires
&lt;/h2&gt;

&lt;p&gt;An agent that remembers usefully needs five properties, and vector similarity gives you none of them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A structured record of what was tried, on what, and what happened.&lt;/li&gt;
&lt;li&gt;A pointer to evidence a human can open in one click.&lt;/li&gt;
&lt;li&gt;A scope narrow enough that the fact is still true where you're applying it.&lt;/li&gt;
&lt;li&gt;A way to be superseded.&lt;/li&gt;
&lt;li&gt;A way to expire.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The record is the easy part. Everyone stops there. The other four are where the work is.&lt;/p&gt;

&lt;p&gt;Here is the table I use. It is deliberately boring, and every column exists because a specific page went badly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;episode&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="kt"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;service&lt;/span&gt;       &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;env&lt;/span&gt;           &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;symptom&lt;/span&gt;       &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- "p99 on /checkout above 4s"&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;        &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- "pool 20 -&amp;gt; 40"&lt;/span&gt;
  &lt;span class="n"&gt;outcome&lt;/span&gt;       &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'fixed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'inconclusive'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;evidence_url&lt;/span&gt;  &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- incident, PR, or log permalink&lt;/span&gt;
  &lt;span class="n"&gt;version_from&lt;/span&gt;  &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;version_to&lt;/span&gt;    &lt;span class="kt"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;observed_at&lt;/span&gt;   &lt;span class="kt"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;superseded_by&lt;/span&gt; &lt;span class="kt"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="nf"&gt;episode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt;    &lt;span class="kt"&gt;timestamptz&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;evidence_url&lt;/code&gt; is not optional and there is no default value. If the agent cannot point at an incident, a pull request, or a permalinked log line, the fact does not get written. I would rather lose a real lesson than store an unattributable one, because the unattributable ones are exactly the ones that come back wrong at 3 a.m. sounding confident.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;version_from&lt;/code&gt; and &lt;code&gt;version_to&lt;/code&gt; are there because most operational facts have a half-life shorter than the documentation about them. "Raising the pool size helped" was true on Postgres 14 with the old pooler. It stopped being true when we swapped the pooler and the fix became "stop opening a connection per request." An agent that stores the first sentence and not the version range is storing a landmine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieval is a filter, not a search
&lt;/h2&gt;

&lt;p&gt;The part I got wrong first was ranking. Semantic search over incidents will happily return the most similar-sounding incident from eighteen months ago on a service that has since been rewritten. Similarity is not relevance.&lt;/p&gt;

&lt;p&gt;Retrieval should filter hard before it ranks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evidence_url&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;episode&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$2&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;superseded_by&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version_to&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;version_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'inconclusive'&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;observed_at&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five rows, not fifty. If the agent needs more than five past episodes to form a hypothesis, it does not have a hypothesis, it has a vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Negative memory needs an expiry date
&lt;/h2&gt;

&lt;p&gt;The most useful thing my agent learned to do was say "the rollback failed last time." The most dangerous thing it learned to do was say "rollbacks don't work."&lt;/p&gt;

&lt;p&gt;Both sentences came out of the same table. The difference is scope. A failed action is evidence about one version, one environment, one failure mode. The moment you let the agent generalize it into a law, you have taught it to refuse the fix that would have worked.&lt;/p&gt;

&lt;p&gt;So negative episodes get the tightest scope you can write and a mandatory &lt;code&gt;expires_at&lt;/code&gt;, default ninety days. If the underlying cause is still true after ninety days, a human will re-record it, and that re-recording is a signal that the problem is structural rather than incidental. If it isn't re-recorded, the memory quietly stops influencing answers. Decay by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Humans write, agents propose
&lt;/h2&gt;

&lt;p&gt;This is the part that gets skipped in every architecture diagram. Writing to memory is a privileged operation, and the agent does not have that privilege.&lt;/p&gt;

&lt;p&gt;The flow I use: the agent proposes an episode with &lt;code&gt;outcome = 'inconclusive'&lt;/code&gt; and the evidence it has. The human who closed the incident converts it to &lt;code&gt;fixed&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt; and confirms the scope. No human, no promotion. The agent's only write is a draft it cannot read back as fact.&lt;/p&gt;

&lt;p&gt;That single rule killed most of my false recalls. It also produced a useful side effect: the promote step is where people noticed that half their "fixes" were actually coincidences, and started writing postmortems that said so.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measure now
&lt;/h2&gt;

&lt;p&gt;Three numbers, all cheap:&lt;/p&gt;

&lt;p&gt;Citation rate. Of the episodes the agent cited in the last thirty days, how many were opened by a human? Anything never opened is either irrelevant or wrong, and I'd like to know which.&lt;/p&gt;

&lt;p&gt;Contradiction rate. How often two retrieved episodes disagree on the same service and action. A high rate means my scope columns are too loose.&lt;/p&gt;

&lt;p&gt;Time to first correct hypothesis. From page start to the first theory a human says "yes, that's plausible" out loud. This is the only metric that matters, and it is the one nobody puts in the blog post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;Memory is a liability with a maintenance cost. Every row you store can be retrieved, cited, and acted on by something with cluster write access at a moment when nobody is awake to check it. That is a different risk class from a search box over your docs.&lt;/p&gt;

&lt;p&gt;If you are building one of these agents this month, and the trend suggests a lot of you are, spend the first day on the schema and the write path, not the embedding model. The model was never the hard part. Knowing whether the thing you just remembered is true is the hard part, and the only durable answer is a link to evidence and a date after which you stop believing it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>sre</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Jensen Huang is gone, and Nvidia has to explain itself on Tuesday</title>
      <dc:creator>Saurav Niraula</dc:creator>
      <pubDate>Sun, 15 Nov 2026 17:30:00 +0000</pubDate>
      <link>https://dev.to/sauravniraula/jensen-huang-is-gone-and-nvidia-has-to-explain-itself-on-tuesday-4154</link>
      <guid>https://dev.to/sauravniraula/jensen-huang-is-gone-and-nvidia-has-to-explain-itself-on-tuesday-4154</guid>
      <description>&lt;p&gt;I've been following Nvidia for years and I still don't have a clean way to describe the last two weeks. So here's the plain version.&lt;br&gt;
Jensen Huang resigned as CEO on November 12. Not a handover, not a planned transition, just gone, effective immediately, after 33 years running the company he co-founded in 1993. Colette Kress, the CFO since 2013, is interim CEO and is somehow still doing the CFO job at the same time.&lt;br&gt;
What ended it wasn't a product miss or a bad quarter. It started on October 29, when the New York Times reported on Huang's relationship with a businesswoman, Alice Huang, whom US intelligence suspects of being a Chinese spy. More reporting followed: an affair with a senior Nvidia employee, and another with someone who reported directly to him. The board opened an independent investigation, and by November 12 it was done.&lt;br&gt;
I want to be careful here. A suspected spy is not a proven spy, and an affair is not automatically a national security breach. Nobody has shown that anything was leaked. But the board decided it couldn't wait for that answer, and once the second relationship with a direct report surfaced, sentiment turned fast. Someone familiar with the decision told CNBC the board worked late into the night and made the call that morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the market did
&lt;/h2&gt;

&lt;p&gt;The stock got hammered. Nvidia shed more than half a trillion dollars in market value at the worst point and closed November 13 at $162.85. That's about 24% below where it sat before the first story, and roughly 33% off its 52-week high of $243.37 from October 6. November is now on track to be Nvidia's worst month since ChatGPT kicked off the boom in late 2022.&lt;br&gt;
Nine investment houses downgraded it. Bernstein halved its price target to $200 and moved to market-perform; Stacy Rasgon called the stock cheap but warned more allegations could still surface. Oppenheimer went to perform and floated a "brain drain," naming AMD, Marvell and Broadcom as the beneficiaries. Stifel kept its buy rating but cut the target hard, from $315 to $205.&lt;br&gt;
Meanwhile Elon Musk and a pile of other tech execs signed a petition asking Nvidia to bring Huang back. It passed 115,000 signatures. Two investors holding about $2.13 billion of stock are pushing the board to at least consider his return. And Trump, predictably, praised him and floated Tim Cook as the replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuesday's earnings call is the real test
&lt;/h2&gt;

&lt;p&gt;Nvidia reports its fiscal third quarter on November 17, after the close. The numbers will probably look great: revenue around $109 billion, up roughly 50% from a year ago, and earnings per share near $2.47. But this is the first earnings call in the company's 27 years as a public company that Jensen Huang won't run. Kress has to make the case five days into the job, while still being the CFO.&lt;br&gt;
I don't envy her. The quarter itself is almost beside the point. Everyone wants to know who runs Nvidia next, what happens to the roadmap over the next year or two, and whether the "Jensen premium" was a real thing that just got deleted. UBS also flagged that Nvidia might pause part of its funding of AI companies, which would pull something like $50 billion out of the ecosystem, plus the risk of tighter chip export rules on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually matters underneath
&lt;/h2&gt;

&lt;p&gt;Huang has said he's willing to testify before Congress before Thanksgiving. Senator Elizabeth Warren's reply was blunt: resigning doesn't absolve him or the company of responsibility for putting national security at risk. There are already two congressional investigations into Nvidia's China sales. Separately, a former OpenAI researcher claims GPU fingerprinting shows models from DeepSeek, Moonshot, MiniMax and Ant were trained on Blackwell chips, despite the export controls meant to stop exactly that.&lt;br&gt;
Take those together and the headline isn't really one CEO's personal life. It's whether the most important company in the AI supply chain can answer hard questions about who it sells to and what it knows. The leadership mess just dragged the question into the open earlier than anyone planned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I land
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend I know what the stock does next. The bulls have a real case: a forward P/E around 11 is cheap for a company growing this fast, and demand for AI compute hasn't gone anywhere. The bears have a real case too: no obvious successor, a board that didn't even have a chair until last week, and a national security cloud that one good earnings report won't clear.&lt;br&gt;
What I'll actually watch on Tuesday isn't the revenue line. It's whether Kress sounds like a caretaker or a CEO, and whether she gives a straight answer about the roadmap and the buyback. That's the first real signal of what Nvidia is for the next two years. Everything else this month has been noise, just noise with an unusually large price tag.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nvidia</category>
      <category>news</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
