<?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: Mudassir Khan</title>
    <description>The latest articles on DEV Community by Mudassir Khan (@mudassirworks).</description>
    <link>https://dev.to/mudassirworks</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3483324%2F3ea1f28d-d1c3-4507-8b66-96771aa3d5c7.webp</url>
      <title>DEV Community: Mudassir Khan</title>
      <link>https://dev.to/mudassirworks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mudassirworks"/>
    <language>en</language>
    <item>
      <title>GitHub Copilot MCP Usage Metrics Without False Signals</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Fri, 30 Oct 2026 18:43:18 +0000</pubDate>
      <link>https://dev.to/mudassirworks/github-copilot-mcp-usage-metrics-without-false-signals-303e</link>
      <guid>https://dev.to/mudassirworks/github-copilot-mcp-usage-metrics-without-false-signals-303e</guid>
      <description>&lt;p&gt;GitHub's latest Copilot usage metrics update finally names things. Instead of lumping every extension into one undifferentiated bucket, the usage metrics API now reports the names of your customer configured skills, custom agents and MCP servers, and each record can carry 25 entries instead of five. That is great news for adoption dashboards, and a trap for anyone who reads "used a lot" as "works well".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The usage metrics API now names your own skills, agents and MCP servers, with 25 entries per record where it used to be five. You can finally see which internal tools people actually touch.&lt;/li&gt;
&lt;li&gt;A usage count is an adoption signal, not a quality signal. Join it to evaluation outcomes before you make any rollout call.&lt;/li&gt;
&lt;li&gt;Telemetry has gaps (clients that never report, telemetry that gets blocked), and instrumentation does not enforce permissions. Treat the numbers as a floor and keep your guardrails somewhere else.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What changed in the Copilot usage metrics API
&lt;/h2&gt;

&lt;p&gt;Short version: named entries, and more of them.&lt;/p&gt;

&lt;p&gt;Before this change, if your org ran three internal MCP servers and a handful of custom agents, the usage data mostly told you that "something custom" got used. Now the record carries the names you configured, which means you can answer questions like "is anyone actually using the read only billing server we shipped last sprint?" without sending a survey nobody fills in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhoczvcfbi58ifjujqd7o.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhoczvcfbi58ifjujqd7o.gif" alt="Before and after card pair: before, five entries per record and one bucket for everything custom; now, 25 entries per record with named skills, agents and MCP servers; each number counts up and its stack of tiles grows" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The jump from five to 25 entries per record matters more than it sounds. With five slots, a busy developer who touched a dozen tools in a day got truncated, and the long tail (usually the internal tools you care about most) fell off the end. At 25, most of that tail survives.&lt;/p&gt;

&lt;p&gt;What it does not tell you: whether the call helped. A connection is not a success, and an invocation is not a merged pull request. That gap is the whole point of this post, and it is the same gap I keep running into when &lt;a href="https://mudassirkhan.me/blog/llm-agent-evaluation-production" rel="noopener noreferrer"&gt;evaluating LLM agents in production&lt;/a&gt;: activity metrics look fantastic right up to the moment you check outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collect named MCP and skills usage
&lt;/h2&gt;

&lt;p&gt;Pull the data on a schedule and land it in your own table. Don't build dashboards straight off the API response. You'll want history, and you'll want to join it with your own data later.&lt;/p&gt;

&lt;p&gt;I'm not going to reproduce GitHub's response schema here; read it from the API docs for your plan and map the field names once. What matters is the normalized shape you store. Here is the normalizer I'd write, with the raw record type kept deliberately loose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// normalize-usage.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Map whatever the usage metrics API returns into one row per named tool, per user, per day.&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RawUsageRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// map from the API's date field&lt;/span&gt;
  &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// map from the API's user identifier&lt;/span&gt;
  &lt;span class="nl"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;skill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mcp_server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the customer configured name, now reported&lt;/span&gt;
    &lt;span class="nl"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolUsageRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;toolKind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;skill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mcp_server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;truncated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the record hit the entry limit, so its tail may be missing&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ENTRY_LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RawUsageRecord&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;ToolUsageRow&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;toolKind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;truncated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;ENTRY_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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in there are easy to skip and expensive later. Normalize tool names, because people rename servers and casing drifts. And keep the &lt;code&gt;truncated&lt;/code&gt; flag: if a record is full at 25, the long tail might still be cut, and your dashboard should say so instead of pretending the count is complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Join event counts to evaluation outcomes
&lt;/h2&gt;

&lt;p&gt;This is where usage metrics turn into something you can actually decide with. Counts tell you who touched a tool. Outcomes tell you whether touching it was worth it. You need both on the same row.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flzvvnc1amm3f7975l7rd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flzvvnc1amm3f7975l7rd.gif" alt="Four step flow from left to right: Usage event, Shared key of tool name and day, Evaluation outcome, Adoption signal; the steps build one after another in reading order" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The join key is the hard part. GitHub's numbers are aggregated, your evaluation results are per run, so pick a grain both sides can honestly share. Tool name plus day is the one I'd reach for. Per user is tempting, but it quietly turns an adoption report into a surveillance report, and people will route around it.&lt;/p&gt;

&lt;p&gt;On your side, emit your own spans for agent operations and their outcomes. OpenTelemetry's semantic conventions (version 1.44.0) include GenAI attributes for operations and for evaluations, which gives you a vendor neutral way to separate "the agent did something" from "what it did was any good":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// record-outcome.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@opentelemetry/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agent-evals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&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;recordToolRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;run&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startActiveSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`execute_tool &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;toolName&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gen_ai.operation.name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;execute_tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gen_ai.tool.name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolName&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="c1"&gt;// Check the GenAI attribute registry for the exact evaluation names in your semconv version.&lt;/span&gt;
      &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gen_ai.evaluation.name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;task_passed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gen_ai.evaluation.score.value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&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="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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;Then the join itself is boring SQL, which is exactly what you want. Aggregate each side first, otherwise the join multiplies your counts:&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;WITH&lt;/span&gt; &lt;span class="k"&gt;usage&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interactions&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;interactions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;BOOL_OR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;truncated&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;any_truncated&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tool_usage&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;evals&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;evaluated_runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ELSE&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="k"&gt;END&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;pass_rate&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tool_evals&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;any_truncated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evaluated_runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pass_rate&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;usage&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;evals&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactions&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;NULL&lt;/code&gt; in &lt;code&gt;evaluated_runs&lt;/code&gt; is not a bug in the query. It is a finding: a tool people use that nobody is measuring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid sampling and telemetry bias
&lt;/h2&gt;

&lt;p&gt;Copilot counts are GitHub telemetry, and telemetry has holes. Three of them will bend your numbers if you ignore them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl6gttwukdvtr3ltbj0co.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl6gttwukdvtr3ltbj0co.gif" alt="Three cards for three telemetry biases: Missing clients, some clients never report; Blocked telemetry, strict networks drop events; Counts are not success, a call is not an outcome; a spotlight visits each card in turn" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Missing clients.&lt;/strong&gt; If a tool is mostly used from a surface that doesn't report, it looks unpopular when it isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocked telemetry.&lt;/strong&gt; Teams with strict network rules, often the same teams running your most sensitive MCP servers, undercount without anyone noticing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counts are not success.&lt;/strong&gt; Interactions and connections measure activity. Only your evaluations measure whether the activity helped.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So treat every count as a floor, compare a tool against its own trend rather than against other tools, and never turn the data into a team leaderboard. My opinion, from building dashboards like this: the first chart someone screenshots is "most used tools", and it is the chart most likely to mislead.&lt;/p&gt;

&lt;p&gt;There is a security angle too. OWASP LLM06 describes excessive agency as coming from three drivers: excessive functionality, excessive permissions and excessive autonomy. Instrumentation alone does not enforce any of those boundaries. A popular MCP server with write access it doesn't need is still a footgun; the usage chart just tells you how many people are holding it. If you're reviewing those boundaries, my &lt;a href="https://mudassirkhan.me/blog/ai-agent-security-prompt-injection" rel="noopener noreferrer"&gt;production defense guide for AI agent security&lt;/a&gt; covers the permission side in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a rollout decision table
&lt;/h2&gt;

&lt;p&gt;Once usage and outcomes sit on the same row, the rollout call stops being a vibe. This is the table I'd pin in the platform team's channel:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;th&gt;Outcomes&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Expand.&lt;/strong&gt; Make it a default and document it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Fix or restrict.&lt;/strong&gt; People want it and it is hurting them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Promote.&lt;/strong&gt; This is a discoverability problem.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Retire.&lt;/strong&gt; Remove the server and its permissions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;No evaluations&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Wait.&lt;/strong&gt; Add evaluation before you decide anything.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwipp2f5m9y1ikcfpeibt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwipp2f5m9y1ikcfpeibt.jpg" alt="Rollout decision matrix with usage on one axis and outcomes on the other: high usage and good outcomes means Expand, high usage and poor outcomes means Fix or restrict, low usage and good outcomes means Promote, low usage and poor outcomes means Retire; a band below reads No evaluations means wait" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last row is the one teams skip. A tool with heavy usage and zero evaluations feels like a win, and that feeling is exactly the false signal this whole setup exists to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I implement github copilot mcp usage metrics safely?
&lt;/h3&gt;

&lt;p&gt;Pull the usage metrics API on a schedule into your own table, normalize tool names, and keep a flag for records that hit the 25 entry limit. Emit your own OpenTelemetry spans for agent operations and evaluation results, join both sides on tool name and day, and make rollout decisions from usage and outcomes together. Keep permission boundaries enforced separately, because instrumentation does not enforce them.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changed with github copilot mcp usage metrics?
&lt;/h3&gt;

&lt;p&gt;GitHub's usage metrics API now reports the names of customer configured skills, custom agents and MCP servers instead of grouping them together, and each record can hold 25 entries instead of five. That makes internal tool adoption visible, but the counts still measure activity, not success.&lt;/p&gt;




&lt;p&gt;Related reading: &lt;a href="https://mudassirworks.hashnode.dev/llm-evaluation-metrics-what-actually-predicts-production-quality" rel="noopener noreferrer"&gt;The LLM evaluation metrics that actually predict production quality&lt;/a&gt; and &lt;a href="https://dev.to/mudassirworks/mcp-server-security-in-production-what-actually-breaks-3n3a"&gt;MCP server security in production: what actually breaks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're still deciding which agent stack to measure in the first place, the &lt;a href="https://mudassirkhan.me/tools/ai-agent-framework-chooser" rel="noopener noreferrer"&gt;AI agent framework chooser&lt;/a&gt; narrows it down in a few questions.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drop a comment if your setup looks different. Curious how people are joining usage data to outcomes in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubcopilot</category>
      <category>mcp</category>
      <category>observability</category>
      <category>ai</category>
    </item>
    <item>
      <title>Google's UGC Fresh Data Program: Freshness Is a Pipeline</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Tue, 27 Oct 2026 18:46:57 +0000</pubDate>
      <link>https://dev.to/mudassirworks/googles-ugc-fresh-data-program-freshness-is-a-pipeline-1p72</link>
      <guid>https://dev.to/mudassirworks/googles-ugc-fresh-data-program-freshness-is-a-pipeline-1p72</guid>
      <description>&lt;p&gt;Google just documented its UGC Fresh Data Program, and the interesting part is not who gets in. It is the architecture it admits to: for eligible high volume user generated content platforms, Google now runs a dedicated ingestion pipeline that is separate from ordinary crawling and separate from the Indexing API. If your forum, Q&amp;amp;A site or review platform lives on fresh content, that is a signal worth reading closely, even if you never apply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The UGC Fresh Data Program is a separate ingestion path into Google for eligible UGC platforms. It is not crawling and it is not the Indexing API, and participation does not guarantee Search visibility.&lt;/li&gt;
&lt;li&gt;Google recommends sending new UGC within minutes and updating engagement counters within 72 hours. That is a freshness contract, not a recrawl schedule.&lt;/li&gt;
&lt;li&gt;Most sites will never qualify, but the lesson transfers: treat every content change as an event, keep content updates apart from counter noise, and make freshness something your system produces on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Google UGC fresh data program is
&lt;/h2&gt;

&lt;p&gt;Think of it as a push lane next to the crawl lane. Normally Google decides when to come back and look at your pages. In this program, an eligible platform sends its fresh content into a dedicated pipeline instead of waiting for the next visit.&lt;/p&gt;

&lt;p&gt;The moment new content gets created is where all of this starts. On most stacks that moment is a form submit, which is why I care so much about &lt;a href="https://mudassirkhan.me/blog/next-js-form-server-actions" rel="noopener noreferrer"&gt;handling Next.js form submissions with server actions&lt;/a&gt; cleanly: if the write path is a mess, nothing downstream can be fresh on purpose.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8kf8zd0aiaapo5ocyb4p.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8kf8zd0aiaapo5ocyb4p.gif" alt="Three paths into Google Search side by side: Ordinary crawling, Google pulls pages on its own schedule; Indexing API, a separate notification channel; UGC Fresh Data Program, eligible platforms push fresh content; a spotlight visits each path in turn" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two details from Google's own documentation matter more than the headline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It is separate.&lt;/strong&gt; Not a faster crawler, not a new mode of the Indexing API. A different pipe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a ranking ticket.&lt;/strong&gt; Participation does not guarantee Search visibility. Getting your content in faster and getting it shown are still two different problems.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why it is not the Indexing API
&lt;/h2&gt;

&lt;p&gt;This is the part I expect people to get wrong first. The instinct is to read "fresh data" and think "great, another way to force URLs into the index." It is not that.&lt;/p&gt;

&lt;p&gt;Think of the Indexing API as a notification channel: you tell Google a URL changed. The UGC Fresh Data Program is an ingestion pipeline for a whole class of platforms, built around how fast content and engagement change on them. The difference shows up in the numbers Google attaches to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18dpd7pmjvy02ehbnfij.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18dpd7pmjvy02ehbnfij.gif" alt="The freshness clock of the UGC Fresh Data Program: new UGC sent within minutes, engagement counters updated within 72 hours, and an application reply in roughly 6 to 8 weeks; each number counts up in turn and its marker grows" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New UGC: within minutes.&lt;/strong&gt; That is the recommendation for fresh posts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engagement counters: within 72 hours.&lt;/strong&gt; Votes, replies and similar signals get their own, slower clock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application response: roughly 6 to 8 weeks.&lt;/strong&gt; This is a selective program, not a self serve API you wire up on a Friday.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is what that split looks like in a type, which is how I'd model it in any codebase that has to reason about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FreshnessLane&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;engagement&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ChangeEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;lane&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FreshnessLane&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;entityId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// The target lag per lane, taken from Google's recommendation for the program.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TARGET_LAG_MS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FreshnessLane&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&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="c1"&gt;// "within minutes"&lt;/span&gt;
  &lt;span class="na"&gt;engagement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&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="c1"&gt;// "within 72 hours"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isLate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChangeEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;TARGET_LAG_MS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lane&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;Five minutes is my own reading of "within minutes", not a number from Google. The point is that content and counters run on different clocks, and your system should know which one a change belongs to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eligibility bar
&lt;/h2&gt;

&lt;p&gt;Google says applicants should expect roughly 6 to 8 weeks for a response, and independent coverage from Search Engine Journal and Search Engine Land confirms the program has a selective eligibility process. So treat this as something a small number of large UGC platforms will use, not a growth hack.&lt;/p&gt;

&lt;p&gt;My opinion, for what it is worth: the platforms that get in will be the ones that can already answer "what changed in the last ten minutes?" from a single table. If that question needs a full database scan or a recrawl of your own site, you are not ready for a push pipeline, whether it points at Google or anywhere else.&lt;/p&gt;

&lt;p&gt;Being ready looks like this. Every write produces an event in the same transaction as the content itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/threads/[id]/actions.ts&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidatePath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;addReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&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="na"&gt;ok&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reply is too short&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Content lane: the page really changed.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;threadId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contentUpdatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changeEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lane&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/threads/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;threadId&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="na"&gt;entityId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/threads/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;threadId&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it from the page with &lt;code&gt;addReply.bind(null, thread.id)&lt;/code&gt; as the form action. The outbox row and the reply commit together or not at all, so a fresh page never exists without an event that says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google UGC fresh data program SEO architecture for ordinary sites
&lt;/h2&gt;

&lt;p&gt;You probably do not qualify. You can still steal the architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw19eglf0l9pb23rt9ahw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw19eglf0l9pb23rt9ahw.gif" alt="A freshness pipeline for ordinary sites in four steps: Content saved, Change logged, Sitemap lastmod updated, Fresh hub pages link to it; the steps build in order from left to right" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Content saved.&lt;/strong&gt; The write path from the previous section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change logged.&lt;/strong&gt; One outbox row per change, tagged with its lane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitemap lastmod updated.&lt;/strong&gt; Only from the content lane. A vote count changing is not a content update, and pretending it is just teaches crawlers your lastmod is noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fresh hub pages link to it.&lt;/strong&gt; New threads show up on pages that already get visited often, so discovery does not depend on one file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The sitemap is where most teams quietly break step 3. Here is the version I'd ship in a Next.js App Router project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/sitemap.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MetadataRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;sitemap&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MetadataRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sitemap&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hidden&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="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;contentUpdatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contentUpdatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://example.com/threads/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastModified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentUpdatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lastModified&lt;/code&gt; reads &lt;code&gt;contentUpdatedAt&lt;/code&gt;, never a generic &lt;code&gt;updatedAt&lt;/code&gt; that moves every time someone upvotes. That one column is the difference between a lastmod signal and a random timestamp.&lt;/p&gt;

&lt;p&gt;The same push versus pull question shows up when you own the index yourself. If you run &lt;a href="https://mudassirkhan.me/blog/hybrid-search-rag" rel="noopener noreferrer"&gt;hybrid search for RAG with keyword and vector retrieval&lt;/a&gt;, the outbox above is exactly what keeps both indexes in sync instead of nightly rebuilds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google UGC fresh data program SEO checklist
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mbxs7h1npjd4h57kzia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mbxs7h1npjd4h57kzia.png" alt="Freshness checklist for UGC sites: every write emits a change event, content and counters on separate lanes, lastmod only from content changes, measure time to fresh, fresh hubs link new pages, apply only if you are a large UGC platform" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every content write emits a change event in the same transaction&lt;/li&gt;
&lt;li&gt;[ ] Content changes and engagement counters live on separate lanes&lt;/li&gt;
&lt;li&gt;[ ] Sitemap &lt;code&gt;lastModified&lt;/code&gt; comes only from real content changes&lt;/li&gt;
&lt;li&gt;[ ] You can measure "time to fresh" per lane from the outbox&lt;/li&gt;
&lt;li&gt;[ ] New pages get linked from hub pages that are already crawled often&lt;/li&gt;
&lt;li&gt;[ ] You apply to the program only if you are a high volume UGC platform, and you plan for a 6 to 8 week wait&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What changed in google ugc fresh data program seo?
&lt;/h3&gt;

&lt;p&gt;Google documented the UGC Fresh Data Program: a dedicated ingestion pipeline for eligible high volume UGC platforms, separate from both ordinary crawling and the Indexing API. Google recommends sending new UGC within minutes and updating engagement counters within 72 hours. Participation does not guarantee Search visibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should teams adopt google ugc fresh data program seo?
&lt;/h3&gt;

&lt;p&gt;Large UGC platforms can apply and should expect roughly 6 to 8 weeks for a response. Everyone else should adopt the architecture instead: emit a change event for every content write, keep content updates apart from engagement counters, drive sitemap lastmod only from real content changes, and link new pages from frequently crawled hubs.&lt;/p&gt;




&lt;p&gt;Related reading: &lt;a href="https://mudassirworks.hashnode.dev/next-js-16-caching-demystified-what-changed-what-broke-and-how-to-fix-it" rel="noopener noreferrer"&gt;Next.js 16 caching demystified: what changed, what broke, and how to fix it&lt;/a&gt; and &lt;a href="https://mudassirworks.hashnode.dev/nextjs-rendering-patterns-rsc-ppr" rel="noopener noreferrer"&gt;Next.js rendering patterns: CSR, SSR, RSC and Partial Prerendering explained&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want a freshness pipeline like this wired into your own product, that is the kind of work I do in &lt;a href="https://mudassirkhan.me/services/nextjs-for-ai-products" rel="noopener noreferrer"&gt;Next.js builds for AI products&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drop a comment if your setup looks different. Curious how people are tracking time to fresh in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>programming</category>
    </item>
    <item>
      <title>Persistent Agents Need a Workspace, Not a Bigger Prompt</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Thu, 22 Oct 2026 18:41:08 +0000</pubDate>
      <link>https://dev.to/mudassirworks/persistent-agents-need-a-workspace-not-a-bigger-prompt-1dc5</link>
      <guid>https://dev.to/mudassirworks/persistent-agents-need-a-workspace-not-a-bigger-prompt-1dc5</guid>
      <description>&lt;p&gt;Vercel Drives just reached general availability: persistent storage that survives individual Sandbox runtimes, with drives that default to 1 TiB and can be configured up to 16 TiB. That release is a good excuse to fix the mistake I see in almost every long running agent I review. Teams treat a persistent agent workspace, sandbox memory and chat history as one thing, then wonder why the agent forgets its own files halfway through a job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agents juggle three kinds of state: conversation memory, workflow checkpoints, and files. Each one has its own lifecycle, and mixing them up is where long tasks break.&lt;/li&gt;
&lt;li&gt;A bigger prompt does not make an agent persistent. A workspace that outlives the runtime does, and that is the gap storage like Vercel Drives fills.&lt;/li&gt;
&lt;li&gt;Give each kind of state its own home, write a small manifest that ties them together, and make resume the default path instead of the exception.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the wider map of agent memory types first, I wrote up &lt;a href="https://mudassirkhan.me/blog/ai-agent-memory-management" rel="noopener noreferrer"&gt;the four kinds of AI agent memory every architect should know&lt;/a&gt;. This post is narrower: what has to survive when the sandbox goes away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three kinds of state agents confuse
&lt;/h2&gt;

&lt;p&gt;Here is the split I use on every agent project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fme5zq68qgl2pt7jcwrup.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fme5zq68qgl2pt7jcwrup.gif" alt="Three kinds of agent state side by side: Conversation memory, the session history that resets with the process; Workflow checkpoints, the graph state keyed by a thread id; Files workspace, the artifacts on a persistent drive; each card rises in turn and its object grows" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversation memory&lt;/strong&gt; is what the model has been told. The OpenAI Agents SDK keeps session history outside a single model call, which is great, but its in process &lt;code&gt;MemorySession&lt;/code&gt; resets when the process exits. That is fine for a chat. It is a footgun for a job that runs for an hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow checkpoints&lt;/strong&gt; are where the agent is in its plan. LangGraph handles this with a checkpointer plus a thread identifier, so a workflow can resume after an interruption, even long after the original process ended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Files&lt;/strong&gt; are what the agent has actually produced: the cloned repo, the generated report, the half finished dataset. Neither of the first two stores holds them, because neither was built to.&lt;/p&gt;

&lt;p&gt;The bug pattern is always the same. Someone stuffs file contents into the conversation so the agent "remembers" them. The prompt grows, the context fills up, and the next run starts from a fresh sandbox with none of the files on disk anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What survives a run
&lt;/h2&gt;

&lt;p&gt;Ask one question about every piece of state: what happens to it when the runtime dies?&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;Lives in&lt;/th&gt;
&lt;th&gt;Survives the process?&lt;/th&gt;
&lt;th&gt;Survives the sandbox?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Conversation memory (in process)&lt;/td&gt;
&lt;td&gt;RAM&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversation memory (persisted session)&lt;/td&gt;
&lt;td&gt;Session store&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflow checkpoint&lt;/td&gt;
&lt;td&gt;Checkpointer, keyed by thread id&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files on the sandbox disk&lt;/td&gt;
&lt;td&gt;Ephemeral disk&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files on a persistent drive&lt;/td&gt;
&lt;td&gt;Drive&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The row that bites people is the fourth one. The agent wrote real work to disk, the checkpoint says "step 7 of 9 done", and the resumed run finds an empty directory. The checkpoint is now lying.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkgy0bswo2f0qs1jkjl4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdkgy0bswo2f0qs1jkjl4.gif" alt="Resume flow across runtimes: Run 1 in Sandbox A writes files to the persistent drive and saves a checkpoint, the sandbox is torn down, Run 2 in Sandbox B mounts the same drive, loads the checkpoint by thread id and continues at the next step; the steps build in order" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why files and memory are different
&lt;/h2&gt;

&lt;p&gt;Memory is about meaning. Files are about bytes. You summarise memory, trim it, and pick what goes back into the prompt. You never summarise a git repo or a parquet file; you either have it or you do not.&lt;/p&gt;

&lt;p&gt;That difference changes how you design for persistent agent workspace sandbox memory architecture. Memory gets a budget and a retrieval strategy. Files get a path, a size limit and an owner. With drives that can grow up to 16 TiB, the size limit is rarely what stops you. Knowing which run wrote what is.&lt;/p&gt;

&lt;p&gt;The cheapest fix I know is a manifest: one small JSON file on the persistent drive that records what each step produced. The checkpoint points at the manifest, and the manifest points at the files. Here is a version you can run with plain Node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stat&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;StepRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;finishedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StepRecord&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WORKSPACE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AGENT_WORKSPACE&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./workspace&lt;/span&gt;&lt;span class="dl"&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;loadManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Manifest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&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="nx"&gt;WORKSPACE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manifest.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Manifest&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;steps&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recordStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Manifest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&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;stat&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="nx"&gt;WORKSPACE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// throws if a file is missing&lt;/span&gt;
  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;finishedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&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;mkdir&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="nx"&gt;WORKSPACE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&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="nx"&gt;WORKSPACE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manifest.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&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;nextStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;step&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="na"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point &lt;code&gt;AGENT_WORKSPACE&lt;/code&gt; at the mounted persistent drive and the same code works on your laptop and in a fresh sandbox. The &lt;code&gt;stat&lt;/code&gt; check is the important bit. A step only counts as done when its files really exist, so a checkpoint can never claim work the disk does not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  A persistence architecture for long tasks
&lt;/h2&gt;

&lt;p&gt;Put the three stores together and you get an architecture that survives a dead sandbox without a bigger prompt.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsooxtjithz5ho5s9izuz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsooxtjithz5ho5s9izuz.gif" alt="Forked worker workspace: one coordinator agent with a shared drive splits a job into three workers, each worker gets its own folder on the drive and writes its results there, then the coordinator merges the folders; a spotlight moves from the coordinator to each worker and its folder grows" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rules I follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One thread id, everywhere.&lt;/strong&gt; The checkpoint key, the manifest folder and the session id share the same id. Debugging a stuck run should start with one string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files on the drive, never in the prompt.&lt;/strong&gt; The prompt gets a short listing of what exists and where. The agent reads a file when it needs it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers get their own folders.&lt;/strong&gt; When the agent forks parallel workers, each one writes under its own path on the shared drive. The coordinator merges at the end. No two workers ever write the same file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume is the default.&lt;/strong&gt; Every run starts by loading the manifest and asking for the next unfinished step. A fresh job is just a manifest with no steps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Which framework you pick for the checkpoint layer matters less than following those rules, but it is still a real decision. The &lt;a href="https://mudassirkhan.me/tools/ai-agent-framework-chooser" rel="noopener noreferrer"&gt;AI agent framework chooser&lt;/a&gt; walks through the tradeoffs if you are still deciding between a graph runtime and a lighter SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistent agent workspace sandbox memory checklist for production
&lt;/h2&gt;

&lt;p&gt;Before you ship a long running agent, check these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Conversation memory is persisted to a session store, not only kept in process&lt;/li&gt;
&lt;li&gt;[ ] Workflow state uses a checkpointer keyed by a thread id&lt;/li&gt;
&lt;li&gt;[ ] Files live on storage that outlives the sandbox (a persistent drive, not the ephemeral disk)&lt;/li&gt;
&lt;li&gt;[ ] A manifest records which step produced which files, and verifies they exist&lt;/li&gt;
&lt;li&gt;[ ] Parallel workers write to separate folders&lt;/li&gt;
&lt;li&gt;[ ] A killed run resumes at the next unfinished step without a human&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My own rule of thumb after a few painful rewrites: if I cannot kill the sandbox in the middle of a job and watch the next run pick up cleanly, the agent is not production ready, no matter how good its answers look.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqp788vx42vff4wji8p6v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqp788vx42vff4wji8p6v.png" alt="Persistence checklist for long running agents: persist the session, checkpoint by thread id, files on a persistent drive, manifest per step, one folder per worker, resume by default" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What changed in persistent agent workspace sandbox memory?
&lt;/h3&gt;

&lt;p&gt;Vercel Drives reached general availability as persistent storage that survives individual Sandbox runtimes, with drives that default to 1 TiB and can be configured up to 16 TiB. That gives agents a place for files that outlives the runtime, which conversation memory and workflow checkpoints were never designed to hold.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should teams adopt persistent agent workspace sandbox memory?
&lt;/h3&gt;

&lt;p&gt;Split agent state into three stores: a persisted session for conversation memory, a checkpointer keyed by a thread id for workflow state, and a persistent drive for files. Tie them together with a small manifest per thread, give parallel workers separate folders, and make every run start by resuming from the manifest.&lt;/p&gt;




&lt;p&gt;Related reading: &lt;a href="https://dev.to/mudassirworks/ai-agent-memory-management-beyond-the-context-window-5c65"&gt;why your AI agent forgets: context engineering patterns that hold up&lt;/a&gt; and &lt;a href="https://medium.com/@mudassir00seven/why-40-of-ai-agent-projects-get-killed-and-the-ones-that-survive-e88ea081da1e" rel="noopener noreferrer"&gt;why 40% of AI agent projects get killed, and the ones that survive&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you are deciding whether a long running agent is worth building at all, the &lt;a href="https://mudassirkhan.me/tools/ai-agent-roi-calculator" rel="noopener noreferrer"&gt;AI agent ROI calculator&lt;/a&gt; is a quick way to sanity check the numbers before you invest in the plumbing.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drop a comment if your setup looks different. Curious how people are splitting memory and files in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>node</category>
    </item>
    <item>
      <title>Next.js 16.4 Is a Migration Release, Not a Version Bump</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:47:16 +0000</pubDate>
      <link>https://dev.to/mudassirworks/nextjs-164-is-a-migration-release-not-a-version-bump-118p</link>
      <guid>https://dev.to/mudassirworks/nextjs-164-is-a-migration-release-not-a-version-bump-118p</guid>
      <description>&lt;p&gt;Next.js 16.4 made Cache Components the recommended path, switched them on by default for new &lt;code&gt;create-next-app&lt;/code&gt; projects, and shipped &lt;code&gt;next upgrade --agent&lt;/code&gt; so an AI agent can drive the upgrade for you. Bumping the version number takes a minute. Moving an existing app onto the new caching model is a migration, and nobody runs it for you, not even the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing apps do not adopt the new caching model on their own. Upgrading the package and adopting Cache Components are two separate jobs, and the second one changes what your pages are allowed to cache.&lt;/li&gt;
&lt;li&gt;Cache Components flip the default question from "which routes are dynamic?" to "which components and functions do I explicitly cache?" That is a contract change, not a config change.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next upgrade --agent&lt;/code&gt; is useful for the mechanical edits. It cannot decide how fresh your data has to be, so keep that decision with a human.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What 16.4 actually changes
&lt;/h2&gt;

&lt;p&gt;Three things land in this release that matter for a production app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cache Components become the recommended path.&lt;/strong&gt; New projects created with &lt;code&gt;create-next-app&lt;/code&gt; get them by default, so the docs, the examples and the next hire's muscle memory all move to this model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;next upgrade --agent&lt;/code&gt; exists.&lt;/strong&gt; The upgrade tool now has an agent oriented mode built for coding agents to run the migration steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Builds got faster for at least one real team.&lt;/strong&gt; An independent production upgrade write up from Roboto Studio reported roughly 5% less shipped JavaScript after moving to 16.4, and a 1,555 page benchmark went from 83.9s total build time on 16.3 to 49.1s on 16.4.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third point is a reason to measure your own app, not a promise. Your page count, your data layer and your hosting all change the number.&lt;/p&gt;

&lt;p&gt;The part that trips people up is in independent migration guidance from iqx.dev: existing applications do not automatically switch to the new caching model, and teams should review caching, navigation and configuration behavior explicitly. So after &lt;code&gt;npm install next@16.4&lt;/code&gt; your app keeps behaving the way it did. A fresh project scaffolded the same afternoon behaves differently.&lt;/p&gt;

&lt;p&gt;The cheapest way to feel that difference is to scaffold a throwaway 16.4 app next to your real one and build something tiny in it, like this &lt;a href="https://mudassirkhan.me/blog/build-calculator-nextjs" rel="noopener noreferrer"&gt;small Next.js calculator project&lt;/a&gt;. Ten minutes in the new defaults teaches you more than an hour of reading release notes, and you get a clean reference to diff against when your real app does something surprising.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cache Components change the migration contract
&lt;/h2&gt;

&lt;p&gt;In the older App Router model you mostly reasoned about caching per route. A route was static or dynamic, and you nudged it with route segment config and &lt;code&gt;fetch&lt;/code&gt; options.&lt;/p&gt;

&lt;p&gt;With Cache Components you reason per component and per function. Anything you want cached, you mark with the &lt;code&gt;use cache&lt;/code&gt; directive and give it a lifetime and tags. Anything that reads request data (cookies, headers, search params) renders at request time and streams in behind a &lt;code&gt;Suspense&lt;/code&gt; boundary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqfy83a6ajc9lddm2a34.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqfy83a6ajc9lddm2a34.gif" alt="Comparison of route level caching (route segment config, fetch cache options, whole route static or dynamic) with Cache Components (use cache per component or function, cacheLife and cacheTag, dynamic parts stream in Suspense); a spotlight moves from the before card to the Cache Components card as each card object grows, then the band You choose what gets cached, explicitly is highlighted" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The opt in is one line of config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cacheComponents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real work is what that line forces you to write next. Here is a product page that mixes shared catalog data with a per visitor cart badge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/products/[id]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/headers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&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;getProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;cacheLife&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hours&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="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;CartBadge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartId&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;cookies&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cart_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cartId&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cartItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cartId&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"badge"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;ProductPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;params&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;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="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="nx"&gt;id&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"badge"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;0&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CartBadge&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;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 decisions live in that file that no tool can make for you. &lt;code&gt;cacheLife("hours")&lt;/code&gt; says a product description may be up to hours old. The &lt;code&gt;Suspense&lt;/code&gt; boundary says the cart badge is per visitor and must never be cached. Get the first one wrong and customers see stale prices. Get the second one wrong and one visitor sees another visitor's cart.&lt;/p&gt;

&lt;p&gt;Invalidation follows the tags. When an admin edits a product, a server action expires exactly that entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/admin/products/actions.ts&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;updateProductPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact signatures of &lt;code&gt;cacheLife&lt;/code&gt; profiles and &lt;code&gt;revalidateTag&lt;/code&gt; have moved between 16.x releases, so check them against the docs for the version you install before you copy this into a real codebase.&lt;/p&gt;

&lt;p&gt;My view after reading through the release and the migration notes: treat every &lt;code&gt;use cache&lt;/code&gt; you add as a written promise about freshness. If you cannot say out loud how stale a piece of data is allowed to be, you are not ready to cache it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What next upgrade --agent can and cannot validate
&lt;/h2&gt;

&lt;p&gt;The agent mode is aimed at a real pain: upgrades are full of mechanical edits that are boring for people and easy for a coding agent. Run it like this, on a branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; upgrade/next-16-4
npx next upgrade &lt;span class="nt"&gt;--agent&lt;/span&gt;
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my opinion the line between what you hand the agent and what you keep is simple: the agent can make the build pass, but only you know whether the passing build is correct.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxnurqixf5wpglpobn5on.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxnurqixf5wpglpobn5on.gif" alt="Two columns build in order: the agent can apply codemods, fix build errors and add Suspense boundaries; the human decides how fresh each page must be, what data is per visitor and when to ship; then the band The agent makes it build. You decide if it is right. wipes in" width="760" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What the agent is good at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;applying codemods and renaming moved imports&lt;/li&gt;
&lt;li&gt;reading build errors and fixing the obvious ones&lt;/li&gt;
&lt;li&gt;wrapping request time reads in a &lt;code&gt;Suspense&lt;/code&gt; boundary when the build demands it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it cannot know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how fresh each page has to be for your business (that is the &lt;code&gt;cacheLife&lt;/code&gt; value)&lt;/li&gt;
&lt;li&gt;which data belongs to one visitor and must never be shared&lt;/li&gt;
&lt;li&gt;whether a navigation or prefetch change breaks a flow your users depend on&lt;/li&gt;
&lt;li&gt;when it is safe to ship&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same split I argue for with any agent that touches production: give it narrow, reviewable permissions and keep the judgment calls with people. Our guide to &lt;a href="https://mudassirkhan.me/blog/mcp-enterprise-agents" rel="noopener noreferrer"&gt;MCP architecture for enterprise agents&lt;/a&gt; goes deeper on drawing those boundaries.&lt;/p&gt;

&lt;p&gt;A practical rule: review the agent's diff with one question per changed file. "Did this edit change what gets cached, or for how long?" If yes, a human signs off on that file.&lt;/p&gt;

&lt;h2&gt;
  
  
  A production migration checklist
&lt;/h2&gt;

&lt;p&gt;Upgrade the version and adopt Cache Components in two separate deploys. Then run the loop below for each route group until the whole app is on the new model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwucya37rr6bum4q4kap.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwucya37rr6bum4q4kap.gif" alt="Upgrade validation loop with four steps that build in order: 01 Upgrade the version, 02 Build and read errors, 03 Mark cache boundaries, 04 Compare in staging, then the band Repeat for each route group" width="760" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;th&gt;Stop the rollout if&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Upgrade the version&lt;/td&gt;
&lt;td&gt;Install 16.4, keep &lt;code&gt;cacheComponents&lt;/code&gt; off, deploy&lt;/td&gt;
&lt;td&gt;Anything changes that you did not expect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Build and read errors&lt;/td&gt;
&lt;td&gt;Turn &lt;code&gt;cacheComponents&lt;/code&gt; on in a branch and run &lt;code&gt;next build&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;You cannot explain an error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Mark cache boundaries&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;use cache&lt;/code&gt;, &lt;code&gt;cacheLife&lt;/code&gt; and &lt;code&gt;cacheTag&lt;/code&gt; per component, &lt;code&gt;Suspense&lt;/code&gt; around request data&lt;/td&gt;
&lt;td&gt;A page mixes shared and per visitor data with no boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Compare in staging&lt;/td&gt;
&lt;td&gt;Diff responses and headers against production, test navigation and prefetch&lt;/td&gt;
&lt;td&gt;Any per visitor data shows up in a cached response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A quick staging check for the scariest failure, a cached response that leaks visitor data:&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;# Two different carts, same product URL. The badge must differ.&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Cookie: cart_id=cart_a"&lt;/span&gt; https://staging.example.com/products/42 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'class="badge"&amp;gt;[0-9]*'&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Cookie: cart_id=cart_b"&lt;/span&gt; https://staging.example.com/products/42 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'class="badge"&amp;gt;[0-9]*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both lines print the same count for carts that hold different items, something per visitor got cached. Stop and find the missing boundary before you go further.&lt;/p&gt;

&lt;p&gt;Then measure. Record build time and shipped JavaScript before and after, the same way the Roboto Studio write up did, so you know whether your app sees anything like their numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What changed in the Next.js 16.4 Cache Components upgrade?&lt;/strong&gt;&lt;br&gt;
Cache Components became the recommended path and are on by default for new &lt;code&gt;create-next-app&lt;/code&gt; projects, and the release added &lt;code&gt;next upgrade --agent&lt;/code&gt;. Existing apps keep their current caching behavior until you opt in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should teams adopt the Next.js 16.4 Cache Components upgrade?&lt;/strong&gt;&lt;br&gt;
Ship the version bump first with Cache Components off. Then enable them on a branch, mark cache boundaries one route group at a time, and compare against production in staging before each rollout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will 16.4 make my build faster?&lt;/strong&gt;&lt;br&gt;
One independent production write up saw a 1,555 page build drop from 83.9s to 49.1s and roughly 5% less JavaScript. Measure your own app before you count on it.&lt;/p&gt;




&lt;p&gt;Related reading: &lt;a href="https://mudassirworks.hashnode.dev/next-js-16-caching-demystified-what-changed-what-broke-and-how-to-fix-it" rel="noopener noreferrer"&gt;Next.js 16 caching demystified: what changed, what broke and how to fix it&lt;/a&gt; and &lt;a href="https://dev.to/mudassirworks/react-server-components-and-ai-streaming-in-nextjs-patterns-that-actually-work-35pc"&gt;React Server Components and AI streaming in Next.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want a team to plan and run this migration on a real product, &lt;a href="https://mudassirkhan.me/services/nextjs-for-ai-products" rel="noopener noreferrer"&gt;Next.js development for AI products&lt;/a&gt; is exactly the kind of work I take on.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drop a comment with how you are splitting the upgrade: one big switch, or route group by route group? Curious what is working in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>Human in the Loop for Computer Use Agents</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Fri, 04 Sep 2026 19:43:18 +0000</pubDate>
      <link>https://dev.to/mudassirworks/human-in-the-loop-for-computer-use-agents-541g</link>
      <guid>https://dev.to/mudassirworks/human-in-the-loop-for-computer-use-agents-541g</guid>
      <description>&lt;p&gt;A computer use agent that hits 62 percent on a hard benchmark is not a product you ship unattended. It's a product you gate. The computer use agent human in the loop question is not philosophical, it's an architecture decision you make per action, and the benchmark numbers tell you exactly where the line goes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The benchmark nobody puts in the pitch deck
&lt;/h2&gt;

&lt;p&gt;You've seen the demo video. Agent opens a browser, fills a form, books the thing, everyone claps. Now here are the numbers that never make the slide.&lt;/p&gt;

&lt;p&gt;On BU Bench V1, a set of 100 hard real world browser tasks, open source browser agents score between 35.2 and 62 percent. The best managed cloud agent scored 78 percent. The top model reached 80 percent, and it burned 580.87 dollars in API cost across that single 100 task run. So the ceiling is roughly four out of five, and the ceiling is expensive.&lt;/p&gt;

&lt;p&gt;WebArena is bleaker. The strongest GPT-4 based agent there managed 14.41 percent end to end task success. Humans on the same tasks hit 78.24 percent. That is not a rounding gap, that's a different category of worker.&lt;/p&gt;

&lt;p&gt;Browser use agent reliability is the number that decides your architecture. Not the demo, not the model card, not the vendor's cherry picked screen recording. If you design assuming 95 percent and reality hands you 50, every downstream assumption you made about error handling, retries, and user trust is wrong at the same time.&lt;/p&gt;

&lt;p&gt;The honest read: these agents are good at proposing actions and bad at guaranteeing outcomes. Build for that and you'll ship something that works. Build for the demo and you'll ship something that quietly corrupts data on a Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why failures compound
&lt;/h2&gt;

&lt;p&gt;Failures don't add up, they multiply. Three agents in a chain, each at a 70 percent success rate, gives you 0.7 times 0.7 times 0.7. That's 34 percent end to end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The math that kills long agent chains&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;perStepSuccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;chainSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;perStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;perStep&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chainSuccess&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="nx"&gt;perStepSuccess&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 0.70&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chainSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;perStepSuccess&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 0.343&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chainSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;perStepSuccess&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 0.168&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chainSuccess&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="nx"&gt;perStepSuccess&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 0.058&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that and stare at the last line for a second. An agent you'd describe in a standup as "usually works" becomes a coin flip you lose 94 percent of the time once you ask it to do eight things in a row.&lt;/p&gt;

&lt;p&gt;Repetition makes it worse independently of chain length. Measured performance drops from about 60 percent on a single run to just 25 percent over 8 consecutive runs. Context fills up, earlier mistakes get treated as ground truth, and the agent starts reasoning from its own bad output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ascrojbb225lg2du4h0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ascrojbb225lg2du4h0.png" alt="Chart showing agent success rate collapsing as chain length grows, from 70 percent at one step to 34 percent at three steps to 6 percent at eight steps" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is why agentic task success rate is such a misleading metric in isolation. A vendor quotes you a per step number because a per step number always looks respectable. The number you actually care about is the one at the end of your chain, and nobody publishes that because it depends on how long your chain is.&lt;/p&gt;

&lt;p&gt;Shorter chains. More checkpoints. That's the whole lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  Co pilot vs autonomous: the architecture decision you make once
&lt;/h2&gt;

&lt;p&gt;The co pilot vs autonomous agent choice sounds like a product positioning question. It isn't. Co pilot means the agent proposes and a human confirms before anything lands. Autonomous means the agent acts and you find out afterwards, from a log or from a customer.&lt;/p&gt;

&lt;p&gt;Most teams pick one mode for the whole product. That's the mistake. The right granularity is the action, not the application.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Reversible?&lt;/th&gt;
&lt;th&gt;Blast radius&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read a page, extract data&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Autonomous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fill a draft form field&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Autonomous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send an email&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;External&lt;/td&gt;
&lt;td&gt;Gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Submit a payment&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Financial&lt;/td&gt;
&lt;td&gt;Hard gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete a record&lt;/td&gt;
&lt;td&gt;Sometimes&lt;/td&gt;
&lt;td&gt;Data loss&lt;/td&gt;
&lt;td&gt;Gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept terms of service&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Legal&lt;/td&gt;
&lt;td&gt;Hard gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same agent, same session, four different trust levels. Reading a dashboard and wiring money are not the same risk just because the same model requested both.&lt;/p&gt;

&lt;p&gt;Once you accept that, the mode stops being a config flag and becomes a classifier that runs before every tool call. Which is more work up front and dramatically less work at 3am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the approval gate
&lt;/h2&gt;

&lt;p&gt;An AI agent approval gate is simpler than it sounds. Before any action executes, you classify it. Safe ones run. Risky ones go into a queue a human can approve or reject. The agent blocks on that queue and continues when the verdict lands.&lt;/p&gt;

&lt;p&gt;Three signals are worth classifying on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reversibility.&lt;/strong&gt; Can you undo this in one step without begging anyone? Reading is reversible. Sending is not. Anything that leaves your system boundary is effectively permanent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius.&lt;/strong&gt; How many rows, users, or dollars does this touch? Updating one draft is not the same as updating a table. Scale the gate with the number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence.&lt;/strong&gt; How sure is the agent? Low confidence plus low blast radius is fine, let it run and check the log. Low confidence plus high blast radius is the exact case the gate exists for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Risk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hard_gate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ActionMeta&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="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;blastRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;external&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;financial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0 to 1, from the agent&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALWAYS_HUMAN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth.submit_credentials&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment.charge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;record.hard_delete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content.publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legal.accept_terms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ActionMeta&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Risk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ALWAYS_HUMAN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hard_gate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reversible&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blastRadius&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;external&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blastRadius&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;financial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blastRadius&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;ALWAYS_HUMAN&lt;/code&gt; is checked first and never consults confidence. That ordering is the point, and I'll come back to why.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnp5vordw2y8mlttzj9ji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnp5vordw2y8mlttzj9ji.png" alt="Decision tree showing an agent action routed through the always human check, then reversibility, blast radius and confidence, ending in auto, review, or hard gate" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wire the gate into the tool layer, not the prompt. A prompt instruction to "ask before deleting" is a suggestion. A classifier in front of the executor is a rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&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;runTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ActionMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requestHumanApproval&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approved&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="na"&gt;ok&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="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected_by_human&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;note&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="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&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;
  
  
  Hybrid HITL: over 95 percent success for 15 seconds of your attention
&lt;/h2&gt;

&lt;p&gt;Here's the number that makes all of this worth building. In a hybrid study combining selective human intervention with autonomous browser agents, success rates improved to over 95 percent across all tested scenarios. Average human intervention time was 15 to 30 seconds per intervention.&lt;/p&gt;

&lt;p&gt;Sit with that trade. You go from a ceiling around 80 percent, at 580 dollars per 100 tasks, to over 95 percent, in exchange for half a minute of somebody's attention at the moments that matter.&lt;/p&gt;

&lt;p&gt;That is a spectacular exchange rate, and it reframes the whole problem. The engineering challenge was never "make the agent fully autonomous". It's "pick the right moments to interrupt".&lt;/p&gt;

&lt;p&gt;Interrupt too often and you've built a slower version of doing it yourself. Users start rubber stamping approvals without reading, which is worse than no gate because now you have the latency and the false confidence. Interrupt too rarely and the compounding failure math from earlier eats you.&lt;/p&gt;

&lt;p&gt;The tuning knob is the confidence threshold and the blast radius mapping in &lt;code&gt;classify&lt;/code&gt;. Log every gated action along with what the human decided, then move the threshold based on real approval rates. If humans approve 99 percent of a given tool's requests without edits, that tool graduates to auto. If they reject or edit often, tighten it.&lt;/p&gt;

&lt;p&gt;Your gate should get quieter over time. If it doesn't, you're not reading your own logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What always needs a human
&lt;/h2&gt;

&lt;p&gt;Some actions never earn autonomy, no matter how good the model gets. Security boundaries such as password input required 100 percent human participation regardless of agent capability. That finding held across capability levels, which is the interesting part. Getting a better model does not move this line.&lt;/p&gt;

&lt;p&gt;Five categories belong in the hard gate permanently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credentials and authentication.&lt;/strong&gt; Passwords, one time codes, session tokens, recovery flows. An agent that can authenticate as you can do everything you can do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments and money movement.&lt;/strong&gt; Charges, transfers, refunds, subscription changes. Money out is the definition of irreversible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Irreversible deletion.&lt;/strong&gt; Hard deletes, dropped tables, emptied trash. Soft delete is reviewable. Hard delete is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publishing anything public.&lt;/strong&gt; Posts, comments, emails, anything with an audience. You cannot unsend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accepting legal terms.&lt;/strong&gt; Contracts, terms of service, consent flows. An agent clicking "I agree" is a liability question, not an engineering one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not confidence threshold questions. There is no score high enough. Put them in a set, check the set first, and never let a heuristic override it. That's why &lt;code&gt;ALWAYS_HUMAN&lt;/code&gt; sits above every other branch in the classifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How reliable are computer use agents right now?&lt;/strong&gt;&lt;br&gt;
Open source browser agents score 35.2 to 62 percent on BU Bench V1's 100 hard tasks. The best managed cloud agent hit 78 percent and the top model reached 80 percent at 580.87 dollars per 100 task run. On WebArena the best GPT-4 based agent managed 14.41 percent versus 78.24 percent for humans. Plan for the benchmark, not the demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should a browser agent pause and ask a human?&lt;/strong&gt;&lt;br&gt;
When the action is irreversible, when it crosses your system boundary, when its blast radius is large, or when agent confidence is low and the blast radius is anything above none. Credentials, payments, hard deletes, publishing and legal acceptance always pause, regardless of confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between co pilot and autonomous agent modes?&lt;/strong&gt;&lt;br&gt;
Co pilot means the agent proposes and a human confirms before execution. Autonomous means the agent executes and you review afterwards. Pick per action rather than per product, since a single session usually contains both safe reads and unsafe writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does adding a human gate cancel out the speed benefit?&lt;/strong&gt;&lt;br&gt;
Not at the measured intervention cost. Selective intervention pushed success past 95 percent while costing 15 to 30 seconds per intervention. The failure mode to watch is gating so often that reviewers start approving without reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three things to verify right now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Grep your tool definitions for anything that authenticates, charges, deletes, publishes or accepts terms. Every hit belongs in an &lt;code&gt;ALWAYS_HUMAN&lt;/code&gt; set today.&lt;/li&gt;
&lt;li&gt;Count the steps in your longest agent chain, raise your measured per step success rate to that power, and compare the result to what you tell users.&lt;/li&gt;
&lt;li&gt;Check whether your "ask before doing X" rule lives in a prompt or in code. If it's in the prompt, move it into the executor.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;If you want a deeper look at how agents get manipulated into taking actions they were never supposed to take, I cover &lt;a href="https://mudassirkhan.me/blog/ai-agent-security-prompt-injection" rel="noopener noreferrer"&gt;AI agent security and prompt injection&lt;/a&gt; in more detail on my site, along with &lt;a href="https://mudassirkhan.me/blog/multi-agent-design-patterns" rel="noopener noreferrer"&gt;multi agent approval patterns&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want this wired up on your own site end to end, &lt;a href="https://mudassirkhan.me/services" rel="noopener noreferrer"&gt;that is exactly the kind of work I take on&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drop a comment if your setup looks different. Curious which actions other people ended up hard gating after getting burned once.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why LLM Memory in Production Fails Silently</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Wed, 02 Sep 2026 19:58:20 +0000</pubDate>
      <link>https://dev.to/mudassirworks/why-llm-memory-in-production-fails-silently-1437</link>
      <guid>https://dev.to/mudassirworks/why-llm-memory-in-production-fails-silently-1437</guid>
      <description>&lt;p&gt;Your agent's memory layer will not throw. It returns three plausible looking chunks, the model answers confidently from them, and nobody notices for a week. That is the real failure mode of LLM memory in production: retrieval quality drifts while every dashboard stays green, so the only defence that actually holds is asserting on what came back before the model ever sees it.&lt;/p&gt;

&lt;p&gt;Here is where memory breaks, what the benchmarks say happens at scale, and the verification hooks I wire around retrieval so the failure gets loud.&lt;/p&gt;




&lt;h2&gt;
  
  
  The silence before the failure
&lt;/h2&gt;

&lt;p&gt;Start with the distinction most teams collapse. Context is what you put in the prompt this turn. Memory is what you can pull back on turn four hundred, in a session that started three weeks ago. Context is a buffer. Memory is a retrieval system, and retrieval systems fail differently from buffers.&lt;/p&gt;

&lt;p&gt;A buffer fails visibly. You blow the window, the API returns an error, you see it in logs. A retrieval system returns something no matter what. Ask it for what the user said about their billing preference and it will hand you the nearest neighbours in embedding space. If nothing relevant exists, the nearest neighbours are still returned, just with lower scores that nobody is reading.&lt;/p&gt;

&lt;p&gt;The model then does exactly what it is trained to do. It writes a fluent answer grounded in whatever you gave it. There is no exception, no 500, no alert. Your error rate is zero and your answers are wrong.&lt;/p&gt;

&lt;p&gt;That is why "why does my AI agent forget things between sessions" is almost never a forgetting problem. The fact is usually sitting in the store. Episodic recall found it during your demo with fifty documents and stopped finding it at fifty thousand, and nothing in the stack was built to notice the difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why vector only retrieval degrades as the corpus grows
&lt;/h2&gt;

&lt;p&gt;The pattern that gets shipped first is always the same: embed everything, store the vectors, fetch the top &lt;code&gt;k&lt;/code&gt; by cosine similarity, stuff them in the prompt. It works beautifully in development. It is also the single most common thing I find at the root of a "the agent got dumber" report.&lt;/p&gt;

&lt;p&gt;Analysis of production memory architectures points the same way: vector only retrieval approaches degrade as corpus size grows, and the primary cause is the retrieval architecture rather than the model on top of it (&lt;a href="https://www.falkordb.com/blog/ai-agent-memory-retrieval-architecture/" rel="noopener noreferrer"&gt;FalkorDB&lt;/a&gt;). Swapping to a stronger model does nothing here, which is exactly why teams burn weeks on it.&lt;/p&gt;

&lt;p&gt;The mechanics are mundane. Similarity is relative, not absolute, so as you add documents the gap between rank one and rank ten compresses until the ordering carries almost no signal. Vector embedding drift compounds it: the store was built with one embedding model, half of it was reindexed with a newer one, and now two chunks about the same fact live in different neighbourhoods. Nothing errors. Precision just leaks.&lt;/p&gt;

&lt;p&gt;Temporal reasoning is where it shows up first, because similarity has no opinion about time. "The user cancelled their subscription" and "the user asked about cancelling" embed almost identically. Both come back. The model picks one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The benchmark reality: LoCoMo says 92.5, BEAM at 10M says 48.6
&lt;/h2&gt;

&lt;p&gt;The published numbers make the scale problem concrete. On the LoCoMo benchmark, the newer Mem0 algorithm scores 92.5 at roughly 6,956 tokens per retrieval call, with sizeable gains over the previous algorithm on both temporal reasoning and questions that chain several facts together (&lt;a href="https://mem0.ai/blog/state-of-ai-agent-memory-2026" rel="noopener noreferrer"&gt;Mem0&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Then the same work measures BEAM, which pushes the corpus toward production size:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Corpus scale&lt;/th&gt;
&lt;th&gt;Leading score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LoCoMo&lt;/td&gt;
&lt;td&gt;benchmark scale&lt;/td&gt;
&lt;td&gt;92.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BEAM&lt;/td&gt;
&lt;td&gt;1M tokens&lt;/td&gt;
&lt;td&gt;64.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BEAM&lt;/td&gt;
&lt;td&gt;10M tokens&lt;/td&gt;
&lt;td&gt;48.6&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;Gain over prior algorithm (LoCoMo)&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Temporal reasoning&lt;/td&gt;
&lt;td&gt;+29.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi hop reasoning&lt;/td&gt;
&lt;td&gt;+23.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the BEAM rows again. The leading system loses roughly a quarter of its score going from 1M tokens to 10M, landing at 48.6. That is the best available system, measured deliberately, not somebody's weekend project. Your store is going to cross 10M tokens faster than you think.&lt;/p&gt;

&lt;p&gt;So the honest answer to "what is the best way to add memory to an LLM agent" is not a product name. It is: pick a reasonable store, then instrument the retrieval step, because whatever you pick is going to degrade along this curve and you need to see it happening.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjh2e2oe6vyxfx7j01111.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjh2e2oe6vyxfx7j01111.png" alt="Chart showing memory system accuracy falling from 92.5 at benchmark scale, to 64.1 at one million tokens, to 48.6 at ten million tokens" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Building verification hooks: what to assert after retrieval
&lt;/h2&gt;

&lt;p&gt;A verification hook is a plain function that runs between the store and the prompt and answers one question: does this result set look like a healthy retrieval, or does it look like the store shrugging?&lt;/p&gt;

&lt;p&gt;Four assertions catch most of it. Start with the shape of the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// verify-retrieval.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MemoryHit&lt;/span&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// cosine similarity, 0 to 1&lt;/span&gt;
  &lt;span class="nl"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// epoch ms&lt;/span&gt;
  &lt;span class="nl"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Assertion&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;VerifyOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;minScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;minHits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;maxAgeDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VerifyOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minHits&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="na"&gt;maxAgeDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyRetrieval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MemoryHit&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;VerifyOptions&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Assertion&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;DEFAULTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxAgeMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxAgeDays&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uniqueSessions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxAgeMs&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;hits&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="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;score&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;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;non_empty&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minHits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;detail&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;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; hit(s) for a &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; char query`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;top_score_above_floor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;minScore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`top score &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no hits&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score_spread_is_meaningful&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;spread&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`spread &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;spread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; across &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; hits`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_stale_dominance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;detail&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;stale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; hits older than &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxAgeDays&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; days`&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;The one people skip is &lt;code&gt;score_spread_is_meaningful&lt;/code&gt;, and it is the one that catches corpus growth. When every hit scores within a hair of every other hit, ranking has stopped ranking. The store is not broken and the scores are not low. They have simply gone flat, which is the compression problem from the previous section showing up as a number you can alert on.&lt;/p&gt;

&lt;p&gt;Then wrap the retriever so nothing calls it raw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// with-verification.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verifyRetrieval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MemoryHit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Assertion&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./verify-retrieval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MemoryHit&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Incident&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;latencyMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;hitCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Assertion&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onIncident&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Incident&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Retriever&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;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyRetrieval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;onIncident&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;latencyMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;hitCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;failed&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="nx"&gt;hits&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;Note what it does not do: it does not block the request. Retrieval quality is a spectrum, and a hook that throws on a soft signal will page you at 3am for a user asking something genuinely novel. Emit the incident, keep serving, and let the rate tell you the story. A steady 2% incident rate is your baseline. The same metric at 15% next month is your corpus growing past what a flat vector index can rank, and now you can see it in a chart instead of a support ticket.&lt;/p&gt;

&lt;p&gt;Wire it up once at the boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawRetriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;memory.retrieval.incident&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;queryPreview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That answers "how do you verify LLM memory retrieval accuracy" in the only way that survives contact with production. Not a one time eval run. A continuous assertion on live traffic, with the score distribution recorded so you can compare this week against last.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memory consolidation: 60% less storage, 22% better precision
&lt;/h2&gt;

&lt;p&gt;Once you can see retrieval health, the highest leverage fix is usually not a better index. It is storing less.&lt;/p&gt;

&lt;p&gt;Raw conversational memory is enormously redundant. The same preference gets restated in six sessions, each turn is embedded separately, and the store fills with near duplicates that all compete for the same slots in your result set. Consolidation collapses those into single canonical facts. In tested deployments that cut storage by 60% and raised retrieval precision by 22% (&lt;a href="https://redis.io/blog/ai-agent-memory-stateful-systems/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The precision gain is the interesting half. Fewer near duplicate vectors means the top results stop being six phrasings of one fact, which directly restores the score spread your hook is watching. Consolidation and verification are the same lever pulled from two ends.&lt;/p&gt;

&lt;p&gt;A cheap first pass, before you reach for anything clever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// consolidation-candidates.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MemoryHit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./verify-retrieval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findDuplicateClusters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MemoryHit&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MemoryHit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MemoryHit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;MemoryHit&lt;/span&gt;&lt;span class="p"&gt;[][]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MemoryHit&lt;/span&gt;&lt;span class="p"&gt;[][]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;hit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;seen&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="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;clusters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;cluster&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="nx"&gt;clusters&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;Run it over a sample of your store and count what comes back. If a meaningful share of your vectors sit in duplicate clusters, you have found your cheapest precision win, and you will pay less for storage on the way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa598rx8fsls4wuzs94u7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa598rx8fsls4wuzs94u7.png" alt="Diagram of a retrieval path where a verification hook sits between the vector store and the prompt and emits incidents to metrics" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Three things to verify right now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Log your score distribution, not just your hits.&lt;/strong&gt; Record top score, bottom score and spread for every retrieval for one day. If the spread is already flat, your ranking stopped working before you noticed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask your store for something it cannot possibly know.&lt;/strong&gt; A made up name, a fact never mentioned. If it returns four confident looking chunks instead of nothing, you have no floor and every empty query is silently answered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count duplicate clusters in a sample.&lt;/strong&gt; Pull a thousand vectors, cluster them at high similarity, and see how many collapse. That number is your consolidation headroom.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the best way to add memory to an LLM agent?&lt;/strong&gt;&lt;br&gt;
Start with the simplest store that fits your access pattern, then instrument the retrieval step before you tune anything. The choice of store matters far less than whether you can see retrieval quality moving. Published benchmarks show every leading system degrading substantially as the corpus grows, so plan for the curve rather than trying to pick your way around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my AI agent forget things between sessions?&lt;/strong&gt;&lt;br&gt;
Usually it did not forget. The fact is in the store and retrieval is no longer surfacing it, because similarity ranking compresses as the corpus grows and older facts lose to newer near duplicates. Check whether the fact is retrievable by direct lookup first. If it is, this is a ranking problem, not a storage problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you verify LLM memory retrieval accuracy?&lt;/strong&gt;&lt;br&gt;
Assert on the result set at request time: non empty, top score above a floor, meaningful spread between best and worst hit, and no domination by stale entries. Emit an incident when an assertion fails, keep serving, and watch the incident rate over weeks. Offline evals tell you how your system did on a fixed set. Only live assertions tell you what it is doing now.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you want a deeper look at how retrieval fits into a system you actually run, I cover &lt;a href="https://mudassirkhan.me/blog/production-rag-guide-2026" rel="noopener noreferrer"&gt;production retrieval architecture&lt;/a&gt; in more detail on my site.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I also wrote about &lt;a href="https://mudassirkhan.me/blog/llm-agent-evaluation-production" rel="noopener noreferrer"&gt;evaluating LLM memory systems&lt;/a&gt; if you want the evaluation side. And if you want this wired up on your own stack end to end, &lt;a href="https://mudassirkhan.me/services" rel="noopener noreferrer"&gt;that is exactly the kind of work I take on&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drop a comment if your setup looks different. Curious what assertions people are actually running on retrieval, and which ones caught something real.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>programming</category>
    </item>
    <item>
      <title>A TypeScript Gatekeeper for AI Agent Tool Calling</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Tue, 01 Sep 2026 20:27:57 +0000</pubDate>
      <link>https://dev.to/mudassirworks/a-typescript-gatekeeper-for-ai-agent-tool-calling-4e46</link>
      <guid>https://dev.to/mudassirworks/a-typescript-gatekeeper-for-ai-agent-tool-calling-4e46</guid>
      <description>&lt;p&gt;Your agent has a &lt;code&gt;deleteRecord&lt;/code&gt; tool. Your OAuth token says it's allowed to call the records API. So when the model decides, halfway through a reasonable looking chain of thought, that cleaning up duplicates means deleting forty rows, nothing in your stack objects. Your token was valid. The tool existed. Off it went.&lt;/p&gt;

&lt;p&gt;That's the gap this post is about, and the fix is smaller than you'd think: a gatekeeper that sits between the model's request and the actual side effect, and answers a question OAuth never asks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why OAuth scope is not enough for agentic tool safety
&lt;/h2&gt;

&lt;p&gt;Scopes are fine at what they do. A scope answers "is this app allowed to touch the calendar API at all", once, at grant time, for the whole session. What it can't answer is "should this specific call, with these specific arguments, in this specific conversation, run right now". Scopes are static and coarse. AI agent tool calling is dynamic and argument dependent, which is a completely different shape of problem.&lt;/p&gt;

&lt;p&gt;The failure data backs this up. Across a survey of 27 benchmark papers and 19 benchmarks, tool invocation and parameter level errors came out as the single biggest failure cluster in LLM agents. Not reasoning. Not retrieval. The call itself, and what got passed into it.&lt;/p&gt;

&lt;p&gt;Meanwhile the surface area keeps growing. Autonomous agents now outnumber humans in enterprise environments at a ratio of 82 to 1, and only 22 percent of organizations treat AI agents as identity bearing entities with formal access controls. So most of these callers have no identity, no policy attached to them, and full use of whatever credential the process happens to hold.&lt;/p&gt;

&lt;p&gt;What's missing is contextual authorization: a decision made per call, with the arguments in hand, that knows who is acting and what the blast radius is. That's the layer we're building.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three tier approval model
&lt;/h2&gt;

&lt;p&gt;The pattern that holds up in production is a three tier model. Auto approve for reads and searches. Notify for actions that land somewhere visible but stay recoverable. Block or require human approval for anything irreversible or high stakes.&lt;/p&gt;

&lt;p&gt;Here's how that splits for a normal SaaS backend:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;searchKnowledgeBase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;auto approve&lt;/td&gt;
&lt;td&gt;Read only, no side effect, cheap to get wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readDocument&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;auto approve&lt;/td&gt;
&lt;td&gt;Same, though watch what the doc contains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sendEmail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;notify&lt;/td&gt;
&lt;td&gt;Visible to a human, embarrassing, survivable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postToSlack&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;notify&lt;/td&gt;
&lt;td&gt;Same shape, plus a delete button exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;createRecord&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;notify&lt;/td&gt;
&lt;td&gt;Additive, easy to reverse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deleteRecord&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;Data loss, and nobody notices until later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;issueRefund&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;Real money leaves the building&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployToProduction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;You know why&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three questions decide where a tool lands. Can you undo it? How far does the damage travel? And would a human notice in time to react?&lt;/p&gt;

&lt;p&gt;Sending a wrong email fails all three gracefully. It's reversible enough (you send a correction), the blast radius is one inbox, and somebody replies within the hour. Deleting a record fails all three badly. It's gone, it quietly breaks whatever referenced it, and you find out during a quarterly report.&lt;/p&gt;

&lt;p&gt;Notice that &lt;code&gt;createRecord&lt;/code&gt; and &lt;code&gt;deleteRecord&lt;/code&gt; sit in different tiers despite hitting the same table. That's the whole point. Tier is about consequence, not about which service you're talking to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building a TypeScript gatekeeper middleware
&lt;/h2&gt;

&lt;p&gt;Three pieces: a policy table keyed by tool name, a decision function, and a wrapper that every call has to pass through.&lt;/p&gt;

&lt;p&gt;Start with the types and the table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CallerContext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;actorRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;member&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readonly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Decision&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Tier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CallerContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Rule&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;searchKnowledgeBase&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="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;

  &lt;span class="na"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@yourcompany.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;external recipient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorRole&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readonly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readonly actor cannot send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;internal recipient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;deleteRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorRole&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`bulk delete of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; rows`&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="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;single row delete by owner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CallerContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Decision&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&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;tool&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rule&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="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool not in allowlist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&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 things worth calling out. The default is deny, so an unknown tool fails closed instead of sliding through. And every rule gets the arguments, not just the tool name, because &lt;code&gt;sendEmail&lt;/code&gt; to a colleague and &lt;code&gt;sendEmail&lt;/code&gt; to your entire customer list are the same tool and very different events.&lt;/p&gt;

&lt;p&gt;That argument inspection is where most of the value lives. Agent RBAC in TypeScript falls out of the same place: &lt;code&gt;CallerContext&lt;/code&gt; carries the acting identity down to the rule, so a member and an owner asking for the same delete get different answers.&lt;/p&gt;

&lt;p&gt;Now the wrapper. Nothing calls a tool directly anymore.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gatekeep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolFn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CallerContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ToolFn&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;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&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;auditLog&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolDeniedError&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;tool&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; blocked: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notifyHumans&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decision&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="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&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;When the gatekeeper denies a call, throw an error the model can read. Agents recover surprisingly well from "you may not delete more than one row at a time" and will usually retry with something narrower. A silent &lt;code&gt;null&lt;/code&gt; just makes it try again identically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to put the policy: execution layer, not model layer
&lt;/h2&gt;

&lt;p&gt;Here's the part teams get wrong, and it's the difference between a control and a suggestion.&lt;/p&gt;

&lt;p&gt;Policy enforcement has to live at the tool execution layer, not inside the agent's reasoning loop. Writing "never delete more than one record" in your system prompt isn't enforcement. It's a request. The model is a suggestion engine, it can be argued out of a rule by a user or by its own chain of thought, and prompt injected content in a retrieved document can push it around too. If you want the same reasoning on how that attack surface works, I wrote about &lt;a href="https://mudassirkhan.me/blog/ai-agent-security-prompt-injection" rel="noopener noreferrer"&gt;AI agent security patterns&lt;/a&gt; separately.&lt;/p&gt;

&lt;p&gt;Enforcement is code that runs after the model has decided and before the side effect happens. That's it. If your rule can be talked around in English, it isn't a rule.&lt;/p&gt;

&lt;p&gt;MCP tool access control gets easy here, because an MCP server is already the chokepoint. Every tool call funnels through one request handler, so you have exactly one place to wrap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CallToolRequestSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;contextFromSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&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;auditLog&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Blocked by policy: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&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="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notify&lt;/span&gt;&lt;span class="dl"&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;notifyHumans&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;runTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&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;One handler, every tool covered, no way for a new tool to ship without inheriting the policy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production patterns: logging, alerting, rollback
&lt;/h2&gt;

&lt;p&gt;Log every decision, including the ones you allowed. Tool name, arguments, tier, reason, actor. That log is the only way you'll ever tune the policy, because on day one you're guessing about tiers and by week three the log tells you which guesses were wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;auditLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CallerContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Decision&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;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agent_tool_audit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;redactSecrets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;Alert on deny spikes rather than on individual denies. A steady trickle is the policy working. A sudden cluster means either a tier is too tight and you're breaking a real workflow, or something is probing you.&lt;/p&gt;

&lt;p&gt;Guard the notify tier from becoming wallpaper. If every notification is routine, people stop reading them, and then notify is just allow with extra steps. Keep the tier small enough that a message in it still means something, and route it somewhere with a response expectation.&lt;/p&gt;

&lt;p&gt;For rollback, bias the whole system toward reversible writes. Soft deletes, append only ledgers, staged changes that need a second call to commit. When tier two mistakes are cheap to undo, you can afford a broader tier two, and the agent gets more useful without getting more dangerous.&lt;/p&gt;

&lt;p&gt;OWASP's Agentic AI Top 10 names Excessive Agency as a critical vulnerability with three root causes: excessive functionality, excessive permissions, and excessive autonomy. Each one maps to a cut you can make today. Excessive functionality means trimming the tool allowlist, because most agents ship with tools nobody uses. Excessive permissions means giving the agent its own scoped credential instead of the service account that can do everything. Excessive autonomy is the tier three list, and it should feel slightly too long rather than slightly too short.&lt;/p&gt;

&lt;p&gt;Once that's running, the audit log doubles as your eval set. Replaying real denied and allowed calls is a much better signal than synthetic cases, which is worth pairing with a proper approach to &lt;a href="https://mudassirkhan.me/blog/llm-agent-evaluation-production" rel="noopener noreferrer"&gt;evaluating agent tool calls&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three things to verify right now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Call a tool that isn't in your policy table and confirm you get a deny, not an execution. Fail closed or you have nothing.&lt;/li&gt;
&lt;li&gt;Send the same tool two argument sets, one benign and one destructive, and confirm they get different tiers.&lt;/li&gt;
&lt;li&gt;Grep your system prompt for the word "never". Every rule you find there needs a matching rule in code.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is excessive agency in AI agents?
&lt;/h3&gt;

&lt;p&gt;OWASP's Agentic AI Top 10 lists it as a critical vulnerability, and it shows up when an agent can do more than its job requires. Three root causes: excessive functionality (tools it never needed), excessive permissions (a credential broader than its task), and excessive autonomy (no approval step on actions you can't undo). Cut any one and the damage from a bad tool call drops.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you control which tools an AI agent can use?
&lt;/h3&gt;

&lt;p&gt;Put a gatekeeper between the model and the tool runtime. It looks up the tool in a policy table, inspects the actual arguments plus the acting identity, and returns allow, notify, or deny. Unknown tools deny by default. Because it runs at the execution layer, the model can't reason its way past it the way it can with a system prompt instruction.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between OAuth scope and tool level authorization?
&lt;/h3&gt;

&lt;p&gt;Scope is granted once and answers whether an app may touch an API at all. Tool level authorization runs on every single call and answers whether this call, with these arguments, from this actor, should proceed. You need both. Scope stops the wrong system connecting, and the gatekeeper stops the right system doing the wrong thing with a perfectly valid token.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you want a deeper look at securing agent tool calls, I cover it in more detail on &lt;a href="https://mudassirkhan.me/blog" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want this wired up on your own stack end to end, &lt;a href="https://mudassirkhan.me/services" rel="noopener noreferrer"&gt;that is exactly the kind of work I take on&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drop a comment if your setup looks different. Curious what tier boundaries people are actually running in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>5 LLM Context Window Myths That Are Costing You Production Bugs</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:41:32 +0000</pubDate>
      <link>https://dev.to/mudassirworks/5-llm-context-window-myths-that-are-costing-you-production-bugs-2o3l</link>
      <guid>https://dev.to/mudassirworks/5-llm-context-window-myths-that-are-costing-you-production-bugs-2o3l</guid>
      <description>&lt;p&gt;The context window is the most misunderstood number in AI engineering. Developers ship LLM features based on what the model card says, hit weird failures in production, and spend hours debugging something that was never going to work the way they assumed.&lt;/p&gt;

&lt;p&gt;I've watched this happen repeatedly. The good news: the failure modes are predictable once you understand what the context window actually does versus what the marketing says it does.&lt;/p&gt;

&lt;p&gt;Here are five myths worth retiring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Myth 1: Advertised Context Size Equals Usable Context
&lt;/h2&gt;

&lt;p&gt;"This model supports 128K tokens" sounds like a promise. It isn't.&lt;/p&gt;

&lt;p&gt;The advertised number is a ceiling, not a target. Independent testing consistently shows models start losing reliability well before their stated limit. Think of it the way you think about RAM: your laptop technically has 16GB, but run Chrome with 40 tabs and notice what happens at 12GB.&lt;/p&gt;

&lt;p&gt;Before your user's first message even arrives, context is already being consumed. The system prompt takes a chunk. Retrieval results take another. Conversation history from previous turns accumulates fast. Tool call results, if you're building agents, can be enormous. By the time the actual question lands, your "128K context model" might have 15K to 25K tokens of usable headroom left.&lt;/p&gt;

&lt;p&gt;Token budget estimation is an active task, not a one time calculation. Measure it every time your infrastructure changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical fix:&lt;/strong&gt; instrument your system to log actual token usage per request. Calculate how much context your infrastructure (system prompt plus retrieval plus conversation history) consumes before the user query arrives. Design your budget around what's left, not around the headline number.&lt;/p&gt;




&lt;h2&gt;
  
  
  Myth 2: Bigger Context Window Always Means Better Performance
&lt;/h2&gt;

&lt;p&gt;Intuitively, you'd expect a model with 200K context to outperform one with 32K on tasks that require broad information synthesis. The benchmarks tell a different story.&lt;/p&gt;

&lt;p&gt;In testing across 13 LLMs, 11 of them dropped below 50% of their baseline accuracy at just 32K tokens. &lt;code&gt;GPT-4o&lt;/code&gt; specifically fell from 99.3% accuracy to 69.7% once the task required genuine reasoning rather than pattern matching against text that happened to be in the prompt.&lt;/p&gt;

&lt;p&gt;Here's why. Attention mechanisms distribute a model's capacity across the entire context. A longer context means each token competes with more other tokens for that attention budget. The model doesn't become smarter as you add tokens. In practice, it often becomes less reliable at locating the specific information you need.&lt;/p&gt;

&lt;p&gt;Noisy context compounds the problem. Retrieving 20 documents when 5 are actually relevant introduces 15 documents worth of distraction that the model still has to process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical fix:&lt;/strong&gt; measure answer quality at different context sizes for your specific workload. Most production teams discover that a well filtered 6K to 10K context outperforms a bloated 50K context. Invest in retrieval precision before investing in window size.&lt;/p&gt;




&lt;h2&gt;
  
  
  Myth 3: The Lost in the Middle Problem Is Solved
&lt;/h2&gt;

&lt;p&gt;You may have seen vendor posts claiming their models have overcome the lost in the middle limitation. Read the benchmarks carefully before you trust that claim.&lt;/p&gt;

&lt;p&gt;The lost in the middle problem is a property of how transformers attend to tokens. Content at the very beginning and the very end of the context window gets more attention. Content buried in the middle gets less. This produces a U shaped performance curve: accuracy is higher for facts placed near the edges and lower for facts placed in the center.&lt;/p&gt;

&lt;p&gt;Even at 4K tokens (small by current standards) accuracy drops from around 75% to 55 to 60 percent for information sitting in the middle of a document. At longer contexts, the middle section can be effectively invisible to the model.&lt;/p&gt;

&lt;p&gt;Models have improved on this. The problem has not been eliminated. Any claim to the contrary should come with benchmark numbers you can inspect yourself.&lt;/p&gt;

&lt;p&gt;If your retrieval pipeline returns documents in relevance order and your most critical document lands at position 3 of 5, you are betting the model will find it. Sometimes it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical fix:&lt;/strong&gt; reorder retrieval results so the highest relevance documents appear at position 1 and the final slot, not in the middle. For long conversation histories, consider summarizing old turns rather than appending them verbatim.&lt;/p&gt;




&lt;h2&gt;
  
  
  Myth 4: Filling the Window Is Always Better
&lt;/h2&gt;

&lt;p&gt;More context means more information means better answers. It seems obvious. It's also wrong in practice.&lt;/p&gt;

&lt;p&gt;Latency and cost scale with context length. On virtually every major LLM API, you pay per token in as well as per token out. A 100K context prompt costs significantly more than a 10K context prompt, is slower to process, and doesn't automatically produce better answers.&lt;/p&gt;

&lt;p&gt;There's a subtler problem: context poisoning. When retrieval returns documents that are topically adjacent but not precisely relevant, you've injected noise. The model now has to reason over that noise to find the actual signal. Sometimes it anchors confidently on the wrong paragraph. This is how hallucination adjacent errors sneak into RAG pipelines that otherwise look correct in unit tests.&lt;/p&gt;

&lt;p&gt;A tight, relevant 8K context frequently outperforms a sprawling 80K context for the same query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical fix:&lt;/strong&gt; treat context length as a variable to optimize, not a setting to maximize. Start with the minimum context that answers the question correctly in testing. Add more only when you can measure that it improves output quality. Establish a cost and latency budget before you scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Myth 5: Every Token Costs the Same
&lt;/h2&gt;

&lt;p&gt;You count total tokens to estimate API cost and stay inside the context limit. But different content tokenizes at very different densities, and some content consumes tokens with almost no reasoning benefit.&lt;/p&gt;

&lt;p&gt;Numbers are the classic case. The year "2026" splits into per digit tokens in most tokenizers. A formatted price like "USD 1,234,567.89" produces roughly 10 to 12 tokens. A table of 50 rows of numeric data can consume two to three times the token budget of a prose summary that communicates the same information.&lt;/p&gt;

&lt;p&gt;Whitespace and verbose structure add up too. Extra blank lines, deeply nested JSON keys, repeated section headers in a long document you're passing as context: all consume tokens that rarely improve the model's reasoning.&lt;/p&gt;

&lt;p&gt;And not all token positions carry equal reasoning weight (see Myth 3). A token deep in the middle of a 100K context contributes less to the output than the same token would near the top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical fix:&lt;/strong&gt; profile your actual prompts with &lt;code&gt;tiktoken&lt;/code&gt; before running them in production. Compress aggressively. Convert raw numeric tables to prose summaries where the model needs to reason about values rather than cite them exactly. Strip decorative whitespace. The goal is information per token, not tokens per request.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;These myths compound each other in production. A team builds on the advertised 128K limit (Myth 1), fills the context with extra retrieval results because more seems better (Myth 4), ignores document ordering because lost in the middle is "solved" (Myth 3), includes raw numeric tables without checking token cost (Myth 5), then wonders why accuracy is erratic at scale.&lt;/p&gt;

&lt;p&gt;The thread connecting all five: treat context as a constrained resource to manage actively. Budget it. Measure quality at different sizes. Prioritize retrieval precision over recall. Optimize for information per token, not tokens per request. Put the most important content where attention is strongest.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does a larger context window improve LLM accuracy?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not automatically. At long contexts, most models show degraded accuracy compared to their baseline. A smaller but well filtered context tends to outperform a large but noisy one for complex reasoning tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the lost in the middle problem?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LLMs pay more attention to content at the start and end of the context window. Information positioned in the middle is attended to less reliably, producing a U shaped accuracy curve across context positions. The effect persists even at short contexts and has not been fully solved by current models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many tokens can you actually use reliably?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This varies by model and task type. As a working rule: calculate your system prompt and retrieval overhead first, then design around what's left. The reliable reasoning budget is typically 40 to 60 percent of the advertised limit for complex tasks, less if your content has high numeric density or many nested structures.&lt;/p&gt;




&lt;p&gt;If you're building AI agents that manage context across multiple turns, these tradeoffs compound fast. I wrote a practical deep dive on &lt;a href="https://mudassirkhan.me/blog/ai-agent-memory-management" rel="noopener noreferrer"&gt;AI agent memory management&lt;/a&gt; that covers session context design, working memory patterns, and when to summarize versus when to retrieve.&lt;/p&gt;

&lt;p&gt;If you've hit a different context window footgun in production, drop it in the comments. I'd love to build a more complete list of the real failure modes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your AI Agent Evaluation Harness Is Lying to You</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Wed, 26 Aug 2026 20:18:47 +0000</pubDate>
      <link>https://dev.to/mudassirworks/your-ai-agent-evaluation-harness-is-lying-to-you-5ekm</link>
      <guid>https://dev.to/mudassirworks/your-ai-agent-evaluation-harness-is-lying-to-you-5ekm</guid>
      <description>&lt;h1&gt;
  
  
  Your AI Agent Evaluation Harness Is Lying to You
&lt;/h1&gt;

&lt;p&gt;Your eval suite is green and your agent is still doing something dumb in production. Both of those things can be true at the same time, and the reason is uncomfortable: AI agent evaluation that only scores the final answer is measuring the wrong thing. An agent can pass every check you have while accessing unauthorized resources, leaking private context, or triggering side effects nobody can undo. The final response looks fine, so the run gets marked successful.&lt;/p&gt;

&lt;p&gt;Here is the part I think most teams get wrong. We ship agents to production with roughly the same evaluation rigor we would apply to a staging demo, then act surprised when the demo grade harness does not catch production grade failures. This one bit me. Below is what a harness has to look at instead, and what to start logging if you log nothing today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agents pass evals, then fail in production
&lt;/h2&gt;

&lt;p&gt;That is not a contradiction. It is a trajectory problem.&lt;/p&gt;

&lt;p&gt;Gartner expects over 40 percent of agentic AI projects to be canceled by the end of 2027, and 32 percent of organizations name quality as the number one deployment barrier. Those numbers are not about models being dumb. They are about teams not being able to tell a good run from a bad one.&lt;/p&gt;

&lt;p&gt;Agent failure is usually trajectory level, not output level. A final answer can look completely acceptable while the intermediate steps show wasted cost, unsafe actions, or planning so brittle it only worked by luck. Your scorer never sees any of that, because your scorer only ever sees the last string.&lt;/p&gt;

&lt;p&gt;Picture a support agent asked to summarize a customer's order history. It returns a correct summary. Green check. What the trace would have shown you is that it hit an expensive search endpoint eleven times because its first three queries were malformed, then pulled the record from an internal table it was never scoped to read. Correct answer. Terrible run. Your eval suite calls that a pass and moves on, and it will keep calling it a pass every night until a bill or an audit makes it someone's problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why final answer only evals miss intermediate failures
&lt;/h2&gt;

&lt;p&gt;Call it final answer bias. You grade one output string, so you can only ever detect defects that show up in that string.&lt;/p&gt;

&lt;p&gt;Three categories slip straight through:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;What actually happened&lt;/th&gt;
&lt;th&gt;Why the output looks fine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unauthorized resource access&lt;/td&gt;
&lt;td&gt;Agent queried a datastore outside its scope&lt;/td&gt;
&lt;td&gt;The answer it produced was still correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private context leakage&lt;/td&gt;
&lt;td&gt;Sensitive context ended up in a tool argument or downstream call&lt;/td&gt;
&lt;td&gt;Leakage happened on the way, not in the reply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Irreversible side effects&lt;/td&gt;
&lt;td&gt;Agent wrote, deleted, or dispatched something it cannot take back&lt;/td&gt;
&lt;td&gt;The confirmation message reads perfectly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of those are detectable from a response string, no matter how good your judge prompt is. That is the whole point. A regression suite built only on final accuracy is not neutral, it is actively reassuring you about the exact class of failure it cannot observe. Green means "the last message looked right." It has never meant "nothing bad happened."&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real metric framework actually covers
&lt;/h2&gt;

&lt;p&gt;There is a published 12 metric evaluation framework for production agents drawn from over 100 deployments. I am not going to recite the twelve names here, because I would be reconstructing them from memory and getting one wrong helps nobody. What I can describe is the shape any serious llm agent evaluation metrics setup has to have.&lt;/p&gt;

&lt;p&gt;Start with &lt;strong&gt;task outcome&lt;/strong&gt;, since that is the one you already have. Did the agent do the thing. Keep it, just stop treating it as the whole score.&lt;/p&gt;

&lt;p&gt;Then &lt;strong&gt;trajectory quality&lt;/strong&gt;, which asks whether the path was sane. Two runs can land on identical answers and deserve wildly different grades.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool call correctness&lt;/strong&gt; is the one I would add next if I could only add one. Right tool, right arguments, right order, correct handling when the call fails. Most bad trajectories are just a pile of bad tool calls wearing a trench coat.&lt;/p&gt;

&lt;p&gt;After that: &lt;strong&gt;cost and token efficiency&lt;/strong&gt;, because agents fail quietly by being expensive long before they fail loudly. &lt;strong&gt;Safety and permissions&lt;/strong&gt;, which is where the three miss categories above finally become measurable. &lt;strong&gt;Latency&lt;/strong&gt;, which nobody cares about until a reasoning loop goes from four steps to nineteen. And &lt;strong&gt;human judgment&lt;/strong&gt;, because some qualities genuinely do not reduce to an automatic scorer, and pretending otherwise just moves the lie somewhere else.&lt;/p&gt;

&lt;p&gt;Categories, not a checklist. Fill them in with metrics you can actually compute against your own system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trace based evals: what to capture
&lt;/h2&gt;

&lt;p&gt;A trace is the honest version of a run. Every tool call, every argument passed, every intermediate step, every retry, every token spent, in order.&lt;/p&gt;

&lt;p&gt;Once you have traces, a tool call audit becomes possible: replay the run and ask whether each call should have happened at all, whether the arguments were well formed, and whether anything in that call touched a resource outside the agent's scope. That is the audit your final answer scorer can never run, because it does not have the material.&lt;/p&gt;

&lt;p&gt;This is also why the strongest harnesses stack four things rather than one. Traces tell you what happened. An eval dataset tells you what should have happened on cases you care about. Production monitoring tells you whether live behavior still matches either of those. Human feedback catches what all three miss. Accuracy alone gives you one number and no way to explain it.&lt;/p&gt;

&lt;p&gt;If you log nothing today, here is Monday morning. Wrap your tool layer so every invocation writes a record before and after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolCallRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;traced&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;A&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCallRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;step&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;A&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;step&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ok&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. One wrapper, one sink, and suddenly every run has a trajectory you can grade instead of a single string you can only trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eval dataset is the bottleneck, not the harness
&lt;/h2&gt;

&lt;p&gt;Teams spend weeks picking a harness and an afternoon writing the cases. It should be the other way around. Agent datasets almost never capture real production failure modes, which is exactly why a demo grade suite passes everything.&lt;/p&gt;

&lt;p&gt;The fix is unglamorous: harvest. Every production run that went sideways, whether a user complained, a retry storm showed up in the logs, or a trace looked wrong on review, becomes a case. Freeze the inputs, record what the trajectory should have looked like, drop it into the regression suite. Do that for a month and you have an eval dataset your competitors cannot copy, because it is made of your own scar tissue.&lt;/p&gt;

&lt;p&gt;If you want the fuller version of how these pieces fit together, I wrote up an &lt;a href="https://mudassirkhan.me/blog/ai-agent-evaluation-framework" rel="noopener noreferrer"&gt;AI agent evaluation framework&lt;/a&gt; with the layering in more detail, and a companion piece on &lt;a href="https://mudassirkhan.me/blog/llm-agent-evaluation-production" rel="noopener noreferrer"&gt;LLM agent evaluation in production&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do you evaluate AI agents in production?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Score the trajectory, not just the reply. Combine traces of every tool call with an eval dataset built from real production failures, live monitoring of the running system, and periodic human review. Task outcome stays in the mix, it just stops being the only signal. The goal is being able to explain why a run passed, not only that it did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What metrics should an agent eval harness measure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cover seven areas rather than one number: task outcome, trajectory quality, tool call correctness, cost and token efficiency, safety and permissions, latency, and human judgment. If you can only add one thing to an existing accuracy check, add tool call correctness. Most bad trajectories are a sequence of bad tool calls, and that metric surfaces them immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do agents pass evals but fail in production?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because evals grade the last message and production grades everything else. An agent can reach a correct answer through an expensive, unsafe, or barely working path, and a final answer scorer has no way to see any of it. Add the fact that eval datasets rarely contain real production failures, and a green suite tells you very little.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three things to verify right now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open your eval config and check whether any assertion reads something other than the final output. If not, that is your gap.&lt;/li&gt;
&lt;li&gt;Pick yesterday's most expensive agent run and count its tool calls. If you cannot count them, you have no trace.&lt;/li&gt;
&lt;li&gt;Look at your last five production incidents. Count how many exist as cases in your regression suite.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;If you want a deeper look at layering traces, datasets, and monitoring together, I cover it in more detail on &lt;a href="https://mudassirkhan.me/blog" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want this wired up on your own site end to end, &lt;a href="https://mudassirkhan.me/services" rel="noopener noreferrer"&gt;that is exactly the kind of work I take on&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drop a comment if your setup looks different. Curious what variations people are running in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Enterprise MCP Gateway: OAuth 2.0 and RBAC in Production</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Sat, 22 Aug 2026 19:52:10 +0000</pubDate>
      <link>https://dev.to/mudassirworks/enterprise-mcp-gateway-oauth-20-and-rbac-in-production-2d5g</link>
      <guid>https://dev.to/mudassirworks/enterprise-mcp-gateway-oauth-20-and-rbac-in-production-2d5g</guid>
      <description></description>
      <category>webdev</category>
      <category>ai</category>
      <category>security</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Vibe Coding Reality Check: 41% More Bugs, 2.74x Flaws</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Tue, 18 Aug 2026 19:32:17 +0000</pubDate>
      <link>https://dev.to/mudassirworks/vibe-coding-reality-check-41-more-bugs-274x-flaws-1bj3</link>
      <guid>https://dev.to/mudassirworks/vibe-coding-reality-check-41-more-bugs-274x-flaws-1bj3</guid>
      <description>&lt;h1&gt;
  
  
  Vibe Coding Reality Check: 41% More Bugs, 2.74x Flaws
&lt;/h1&gt;

&lt;p&gt;You ship faster with a model in your editor. You also ship more defects, and the gap is wider than most teams assume: one study put the bug rate increase at 41% after teams adopted AI coding tools, and security flaws turn up roughly 2.74x more often in AI assisted code.&lt;/p&gt;

&lt;p&gt;That is not an argument for turning the tools off. Around 95% of code reaching production now contains AI assisted content, so the real question stopped being whether you use them. It became how you stop quality from quietly draining out while you enjoy the speed.&lt;/p&gt;

&lt;p&gt;Here is what the data actually says, why it happens, and the five practices that hold the line without putting you back on the slow path.&lt;/p&gt;




&lt;h2&gt;
  
  
  The vibe coding moment: what actually happened
&lt;/h2&gt;

&lt;p&gt;Vibe coding is the habit of describing what you want, accepting whatever the model writes, and judging the result by whether it runs. Prompt, run, prompt again. No line by line read of the diff.&lt;/p&gt;

&lt;p&gt;It feels incredible. A feature that used to eat an afternoon lands in twenty minutes. Cursor, Copilot and Claude Code all got good enough at the same time that the friction of writing code dropped below the friction of reviewing it, and that inversion is the whole story.&lt;/p&gt;

&lt;p&gt;Because when writing gets cheap and reviewing stays expensive, people write more and review less. Not out of laziness. Out of arithmetic. A tool that produces 300 lines in one shot does not produce 300 lines of review attention alongside it, and nobody budgeted for the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Uplevel study: 41% bug rate increase after AI adoption
&lt;/h2&gt;

&lt;p&gt;The number that keeps getting quoted is Uplevel's: teams that adopted AI coding tools saw a &lt;strong&gt;41% increase in bug rates&lt;/strong&gt;. The mechanism identified was over reliance, developers taking suggestions without enough review behind them.&lt;/p&gt;

&lt;p&gt;CodeRabbit's research points the same direction from a different angle. AI assisted codebases carried &lt;strong&gt;1.7x more issues per pull request&lt;/strong&gt; than human only code. Not per line. Per pull request, which is the unit your team actually reviews, so the extra load lands squarely on whoever is doing the reading.&lt;/p&gt;

&lt;p&gt;Sit with that second number for a second, because it explains the first one. If every PR now carries almost twice the issues, and your review process did not change, your review process is now catching a smaller fraction of what is there. The bugs are not appearing out of nowhere. They are walking through a gate that was sized for a different volume.&lt;/p&gt;

&lt;p&gt;What makes this hard to notice is that the failures are boring. Not exotic model hallucinations. An error path that swallows the exception. A null check that reads correctly and is placed one branch too late. Code that passes review because it looks like code you would have written, which is exactly what these models are optimised to produce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security vulnerabilities: 2.74x more flaws in AI assisted codebases
&lt;/h2&gt;

&lt;p&gt;Static analysis across enterprise repositories found security vulnerabilities roughly &lt;strong&gt;2.74x more prevalent&lt;/strong&gt; in AI assisted code than in manually written code. That multiplier is the one worth taking to your lead, because security defects have a cost curve nothing else in this list matches.&lt;/p&gt;

&lt;p&gt;Why security specifically? A model writes what is statistically typical for the surrounding context. Typical code on the public internet includes a lot of tutorial grade shortcuts: string concatenation into queries, permissive CORS, secrets read straight from a literal, validation that trusts the shape of an object because the type annotation said so. None of it looks wrong. All of it is a footgun in a real deployment.&lt;/p&gt;

&lt;p&gt;The model also has no idea where your trust boundary sits. It cannot know that this particular handler is reachable without auth, or that this input crossed the network two frames ago. That context lives in your head and in your architecture, and it is precisely the context that decides whether a piece of code is fine or a hole.&lt;/p&gt;

&lt;p&gt;Type systems do not save you here either. Type safe code can be perfectly type safe and still authorise the wrong user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why developers still trust AI despite the data (and when that trust breaks)
&lt;/h2&gt;

&lt;p&gt;Ask a room of engineers whether AI tools make them faster and almost every hand goes up. Ask whether the code is better and the hands drop. Both answers are honest, and they are not in conflict.&lt;/p&gt;

&lt;p&gt;The trust holds because the failure mode is delayed. You feel the speed instantly, in the same session. You feel the defect three weeks later, in an incident channel, usually attributed to something else entirely. Nothing in that loop connects the two events, so the feedback that would calibrate your trust never arrives.&lt;/p&gt;

&lt;p&gt;Trust breaks in exactly one situation: the first time someone traces a production incident back to a block of code nobody on the team can explain. Not because it is complicated, but because no human ever really read it. That moment lands differently than any statistic, and it is the moment most teams finally add gates.&lt;/p&gt;

&lt;p&gt;You do not need to wait for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five practices that preserve quality without killing speed
&lt;/h2&gt;

&lt;p&gt;None of these ask you to write less with AI. They move the cost from your future incident channel to your current pipeline, where it is cheaper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Read the diff like it came from a stranger.&lt;/strong&gt; Not the prompt. Not the explanation the model gave you. The diff. If you would send it back when a contractor you had never met submitted it, send it back now. The strongest version of this rule is a personal one: never merge code you could not defend in an incident review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Make static analysis blocking, not advisory.&lt;/strong&gt; A SAST tool that posts a comment gets ignored inside a week. One that fails the build gets fixed. Given a 2.74x security multiplier, this is the single highest leverage change on the list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/security.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semgrep/semgrep&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="c1"&gt;# --error makes findings exit nonzero, which fails the check&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semgrep ci --config auto --error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Cap the blast radius per pull request.&lt;/strong&gt; Review quality falls off a cliff past a few hundred changed lines, and AI makes large diffs trivially easy to produce. Put a real limit in front of yourself:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# .git/hooks/pre-push  (chmod +x this file)&lt;/span&gt;
&lt;span class="c"&gt;# Refuse to push a branch that has grown past a reviewable size.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE_BRANCH&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;/main&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;LIMIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DIFF_LIMIT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;400&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;changed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--numstat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;...HEAD &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{ added += $1; removed += $2 } END { print added + removed + 0 }'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$changed&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LIMIT&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Branch changes &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;changed&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; lines, limit is &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LIMIT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Split it, or push with DIFF_LIMIT=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;changed &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt; if you have a reason."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Test the boundaries the model cannot see.&lt;/strong&gt; Auth, authorisation, input validation, error paths, resource cleanup. A model writes the happy path beautifully because the happy path is what most public code demonstrates. Write those tests yourself, or at minimum write the test names yourself so the shape of the contract comes from you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Track one quality signal per pull request.&lt;/strong&gt; Issues found in review, escaped defects, whatever your team already counts. You cannot manage a 41% drift you never measured, and the whole danger of this failure mode is that it is invisible month to month. A single number, tracked over eight weeks, tells you more than any benchmark someone else published.&lt;/p&gt;

&lt;p&gt;The pattern underneath all five: the model generates, and a gate that does not get tired verifies. Human attention is the scarce resource now, so spend it on the parts machines cannot check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things to verify right now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;semgrep ci --config auto&lt;/code&gt; against your main branch and count the findings. That is your current baseline, whether you knew it or not.&lt;/li&gt;
&lt;li&gt;Check whether your SAST job is set to fail the build or just comment. If it comments, flip it.&lt;/li&gt;
&lt;li&gt;Pull the last ten merged pull requests and check the diff size with &lt;code&gt;git diff --numstat&lt;/code&gt;. If the median is over 400 lines, your review process is already running past its limit.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does AI generated code introduce more bugs?&lt;/strong&gt;&lt;br&gt;
The available data says yes. One study measured a 41% increase in bug rates after teams adopted AI coding tools, and separate research found 1.7x more issues per pull request in AI assisted codebases. The cause identified in both cases is reduced review depth rather than the model producing nonsense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is vibe coding?&lt;/strong&gt;&lt;br&gt;
Describing what you want to a model, accepting the generated code, and validating it by running it rather than reading it. It works well for prototypes and throwaway scripts. It degrades badly once the code has users, because "it runs" and "it is correct" stop being the same claim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you maintain code quality when using AI tools?&lt;/strong&gt;&lt;br&gt;
Move verification into automation and keep human attention on judgment. Blocking static analysis, small reviewable diffs, tests you wrote for the boundaries the model cannot see, and one tracked quality metric will cover most of the gap. The tools are not the problem. An unchanged review process running at several times its designed volume is.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you want a deeper look at how these gates fit into an AI system you actually run in production, I cover it in more detail on &lt;a href="https://mudassirkhan.me/blog" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want this wired up on your own codebase end to end, &lt;a href="https://mudassirkhan.me/services" rel="noopener noreferrer"&gt;that is exactly the kind of work I take on&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drop a comment if your setup looks different. Curious what gates people are actually running, and which ones survived contact with a deadline.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>security</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Zod vs Valibot in 2026: Bundle Size and Speed, Compared</title>
      <dc:creator>Mudassir Khan</dc:creator>
      <pubDate>Sat, 15 Aug 2026 19:32:25 +0000</pubDate>
      <link>https://dev.to/mudassirworks/zod-vs-valibot-in-2026-bundle-size-and-speed-compared-1kng</link>
      <guid>https://dev.to/mudassirworks/zod-vs-valibot-in-2026-bundle-size-and-speed-compared-1kng</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnoj7www9nxz9cmemhbcg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnoj7www9nxz9cmemhbcg.png" alt=" " width="1000" height="400"&gt;&lt;/a&gt;# Zod vs Valibot in 2026: Bundle Size and Speed, Compared&lt;/p&gt;

&lt;p&gt;For about two years the pitch for Valibot was one line long: it's the tiny one. That was enough. If you were shipping schemas to the browser, the weight difference was impossible to ignore, and plenty of teams migrated on that argument alone.&lt;/p&gt;

&lt;p&gt;Then Zod v4 landed with a 14x string parsing speed gain and a stripped down build called Zod Mini, and the tiny one argument suddenly needed a rematch. So here are the actual numbers, where each library still wins, and which one I'd reach for on a new project today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this choice costs you twice
&lt;/h2&gt;

&lt;p&gt;Validation is one of the few pieces of code that gets taxed on both sides of the wire.&lt;/p&gt;

&lt;p&gt;On the client it ships. Every user who loads your page downloads your schema library before they can type into a form, and it sits in the same budget you fight over when you're arguing about whether to lazy load a chart component.&lt;/p&gt;

&lt;p&gt;On the server it runs hot. Every request that crosses your API boundary gets parsed. Every webhook payload, every query param, every row you pull back from an untrusted source. If your validator is slow, that slowness compounds at exactly the point where you have the least headroom.&lt;/p&gt;

&lt;p&gt;Most library choices only hit one of those. A charting library is bundle weight and nothing else. A queue worker is runtime cost and nothing else. Schema validation is both, which is why the argument gets so loud for a dependency that most people write maybe forty lines against.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ntlav60d7olk0f6115a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ntlav60d7olk0f6115a.png" alt="Bar chart comparing minified and gzipped bundle size: Valibot 1.37 kB, Zod Mini 6.88 kB, Zod Standard 17.7 kB" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundle size: the three numbers that matter
&lt;/h2&gt;

&lt;p&gt;Here's the comparison people keep getting wrong, because there are three builds in play and not two.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;Minified + gzipped&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Valibot (core)&lt;/td&gt;
&lt;td&gt;~1.37 kB&lt;/td&gt;
&lt;td&gt;Modular functions, pruned per import&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zod Mini&lt;/td&gt;
&lt;td&gt;~6.88 kB&lt;/td&gt;
&lt;td&gt;Tree shakeable subpackage added in v4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zod Standard&lt;/td&gt;
&lt;td&gt;~17.7 kB&lt;/td&gt;
&lt;td&gt;Full chainable API, one large surface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zod Standard is heavy for a reason that isn't sloppiness. The chainable API means &lt;code&gt;z.string()&lt;/code&gt; returns an object carrying every refinement method you might call next, so your bundler can't confidently prove that &lt;code&gt;.datetime()&lt;/code&gt; is dead code when you never touch it. One big connected surface prunes badly.&lt;/p&gt;

&lt;p&gt;Valibot goes the other way. Everything is a standalone function you import by name, so the bundler does what bundlers are good at: it drops what you never referenced. Import three validators, ship three validators.&lt;/p&gt;

&lt;p&gt;The mistake I see constantly in 2026 is people quoting the 17.7 kB figure at Valibot. That comparison was fair in the Zod v3 era. It isn't now. If bundle size is your reason for looking at Valibot, the honest matchup is 1.37 kB against 6.88 kB, and that's a much narrower gap than the one that started the migrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zod v4: what actually changed
&lt;/h2&gt;

&lt;p&gt;Two things, and they happen to be the exact two complaints that pushed people out.&lt;/p&gt;

&lt;p&gt;Speed came first. String parsing got roughly 14x faster, which is the kind of jump you notice in a load test rather than in a demo. Then Zod Mini arrived as a separate entry point built for tree shaking, which finally gave Zod an answer to the weight question instead of a shrug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Zod Standard: chainable, reads well, prunes badly&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Zod Mini: same engine, function-first entry point, tree shakeable&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod/mini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be honest about where the speed actually lands, though. A signup form validating one object on submit will never notice 14x. What notices is the API gateway parsing thousands of payloads a minute, or the ingestion job running a schema across a large array of rows. If validation isn't on your hot path, treat the speed number as nice rather than as a reason to migrate.&lt;/p&gt;

&lt;h2&gt;
  
  
  API ergonomics: where each one wins
&lt;/h2&gt;

&lt;p&gt;Zod wins on familiarity, and familiarity is worth more than developers like to admit. The chained style reads like a sentence, your editor autocompletes you through it, and roughly every tutorial, every form library adapter, and every Stack Overflow answer written since 2022 assumes you're holding a Zod schema. That ecosystem gravity is the real product.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;valibot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Valibot wins on honesty. That import line at the top is a literal bill of materials for what you're about to ship, and after a week the pipe style stops feeling like ceremony and starts feeling like composition. You can pull a validator out into a named constant and reuse it across schemas without wrapping anything.&lt;/p&gt;

&lt;p&gt;It has a real learning curve, though, and I'd rather say that plainly than pretend otherwise. Everyone arriving from Zod finds it verbose at first, error message customisation takes a minute to locate, and if a teammate is debugging a schema at 2am they will be slower in the style they've never written. That cost is temporary but it's not zero, and on a team of eight it's eight times whatever it is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl9i92vn4zyjvu7dkjvfs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl9i92vn4zyjvu7dkjvfs.png" alt="Decision tree: is the schema client side, is the codebase greenfield, how tight is the bundle budget, leading to Zod Standard, Zod Mini or Valibot" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When to pick which
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server only code, no client bundle&lt;/td&gt;
&lt;td&gt;Zod Standard&lt;/td&gt;
&lt;td&gt;Weight is irrelevant, ergonomics and ecosystem win&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing Zod codebase&lt;/td&gt;
&lt;td&gt;Zod Mini&lt;/td&gt;
&lt;td&gt;Same engine, no rewrite, most of the size back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Greenfield, client heavy, tight budget&lt;/td&gt;
&lt;td&gt;Valibot&lt;/td&gt;
&lt;td&gt;Smallest footprint, and no migration cost to pay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publishing a library&lt;/td&gt;
&lt;td&gt;Valibot&lt;/td&gt;
&lt;td&gt;Your kilobytes become your users' kilobytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team optimising for velocity&lt;/td&gt;
&lt;td&gt;Zod&lt;/td&gt;
&lt;td&gt;Ecosystem answers questions before you ask them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one I'd push back on hardest is migrating a large existing Zod codebase purely for kilobytes. Switching a few hundred schemas costs you review time, a window of subtle behaviour differences in error shapes, and every integration that expects a Zod schema. Moving from Zod Standard to Zod Mini gets you most of the size back for a fraction of that risk. Do the cheap thing first, measure, and only then decide whether the last few kilobytes are worth a rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Valibot better than Zod?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not universally. Valibot is smaller and prunes better, so it wins where bundle weight is the constraint. Zod wins on ecosystem, on how many libraries already speak its schemas, and on how quickly a new teammate becomes productive. Pick against your actual constraint rather than against a benchmark someone posted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changed in Zod v4?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;String parsing got roughly 14x faster, and a new Zod Mini subpackage arrived that's built for tree shaking. Together those addressed the two reasons people were leaving: it was slow on hot paths and it was heavy in the browser. If you evaluated Zod before v4 and walked away, your evaluation is out of date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which TypeScript validation library has the smallest bundle?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Valibot, at roughly 1.37 kB minified and gzipped for the core package. Zod Mini sits at about 6.88 kB and Zod Standard at about 17.7 kB. Just make sure you compare against the build you'd actually ship, because quoting the Standard figure against Valibot overstates the gap by a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;p&gt;For most teams in 2026 the answer is Zod Mini. You keep the ecosystem, you keep the muscle memory, and you give up a handful of kilobytes to Valibot rather than the ten plus you were giving up before v4.&lt;/p&gt;

&lt;p&gt;Reach for Valibot when you're starting fresh and the client bundle is genuinely the thing you're optimising. The old size argument still points at Valibot, it just doesn't win the whole debate by itself anymore.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you want a deeper look at how validation fits into a production TypeScript stack, I cover it in more detail on &lt;a href="https://mudassirkhan.me/blog" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want this wired up on your own site end to end, &lt;a href="https://mudassirkhan.me/services" rel="noopener noreferrer"&gt;that is exactly the kind of work I take on&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drop a comment if your setup looks different. Curious what people are actually running for schema validation in 2026, and whether anyone has migrated back.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
