<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Agent on Code is cheap, let&#39;s talk</title>
    <link>https://blog.ferstar.org/en/tags/agent/</link>
    <description>Code is cheap, let&#39;s talk</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <copyright>© 2026 ferstar · [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)</copyright>
    <lastBuildDate>Mon, 31 Aug 2026 23:41:00 +0800</lastBuildDate>
    <ttl>60</ttl><atom:link href="https://blog.ferstar.org/en/tags/agent/index.xml" rel="self" type="application/rss+xml" /><image>
      <url>https://blog.ferstar.org/site-logo.png</url>
      <title>Code is cheap, let&#39;s talk</title>
      <link>https://blog.ferstar.org/</link>
    </image>
    
    <item>
      <title>Optional Plugins Must Not Block the Core Session: MCP Startup Isolation and Graceful Degradation</title>
      <link>https://blog.ferstar.org/en/posts/mcp-server-startup-isolation-and-graceful-degradation/</link>
      <pubDate>Mon, 31 Aug 2026 23:41:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/mcp-server-startup-isolation-and-graceful-degradation/</guid>
      <description>A single timeout or crash across configured MCP servers can fatally crash an entire agent session during startup; design service-level criticality contracts, concurrent startup sandboxes, and dynamic tool filtering; achieve resilient fault isolation and seamless degradation for non-essential external tools.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>With the Model Context Protocol (MCP) becoming widely adopted, attaching multiple local or remote MCP servers to an agent runtime is standard practice.</p>
<p>In real workloads, however, running multiple MCP servers quickly exposes a fragility issue:</p>
<blockquote><p><strong>You have five MCP servers in your configuration. Four core local tools for filesystem and terminal work are healthy, but an optional third-party translation or web search server times out or fails due to a local dependency mismatch.</strong></p>
<p><strong>The entire agent runtime panics during initialization. You cannot even ask basic questions or edit local files.</strong></p>
</blockquote><p>A non-essential auxiliary plugin crash shouldn’t take down the entire core session.</p>
<p>To resolve this, we introduced concurrent startup isolation and graceful degradation for MCP services.</p>
<pre class="not-prose mermaid">
flowchart TD
  subgraph Config[MCP Service Criticality]
    C1[Critical: Local Filesystem & Terminal]
    C2[Optional: Remote Knowledge & Search]
  end

  subgraph Startup[Concurrent Isolation Sandbox]
    C1 --> T1[Tokio Task 1: Critical Service, Strict Validation]
    C2 --> T2[Tokio Task 2: Isolated 3s Timeout]
    C2 --> T3[Tokio Task 3: Isolated 3s Timeout]
  end

  subgraph Outcome[Aggregation & Dynamic Degradation]
    T1 -->|Success| R[Dynamic Tool Registry]
    T2 -->|Timeout / Error| D[Log Diagnostic Warning, Don't Block]
    T3 -->|Success| R
    D -.->|Filter Unavailable Tools| R
    R --> S[Session Launches Cleanly / UI Notice: Degraded Mode]
  end

  Config --> Startup
</pre>

<hr>

<h2 class="relative group">1. What Was Wrong With the Legacy Flow?
    <div id="1-what-was-wrong-with-the-legacy-flow" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-what-was-wrong-with-the-legacy-flow" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Many MCP clients initialize servers via a simple sequential loop:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="c1">// Fragile serial loop
</span></span></span><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="n">server_config</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="n">mcp_servers</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">// If any connect or list_tools call fails, the entire setup returns Err
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">let</span><span class="w"> </span><span class="n">client</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">McpClient</span>::<span class="n">connect</span><span class="p">(</span><span class="o">&</span><span class="n">server_config</span><span class="p">).</span><span class="k">await</span><span class="o">?</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">let</span><span class="w"> </span><span class="n">tools</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">client</span><span class="p">.</span><span class="n">list_tools</span><span class="p">().</span><span class="k">await</span><span class="o">?</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">registered_tools</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">tools</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>The flaws are obvious:</p>
<ol>
<li><strong>Cumulative startup latency</strong>: Total startup time is the sum of every server’s handshake. A single slow server stalling for 5 seconds delays the whole agent by 5 seconds.</li>
<li><strong>Missing fault isolation</strong>: Core filesystem tools and auxiliary search tools share the same lifecycle. An external timeout escalates into a fatal crash.</li>
</ol>
<hr>

<h2 class="relative group">2. The Solution
    <div id="2-the-solution" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-the-solution" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The revised flow focuses on three changes:</p>

<h3 class="relative group">1. Explicitly Distinguish Critical from Optional Services
    <div id="1-explicitly-distinguish-critical-from-optional-services" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-explicitly-distinguish-critical-from-optional-services" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>We added a criticality flag to the configuration schema:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"mcpServers"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"filesystem"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">"command"</span><span class="p">:</span> <span class="s2">"agent-mcp-fs"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">"required"</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"web_search"</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">"command"</span><span class="p">:</span> <span class="s2">"uvx"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">"args"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"mcp-server-duckduckgo"</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">      <span class="nt">"required"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">"timeout_ms"</span><span class="p">:</span> <span class="mi">3000</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<ul>
<li><code>required: true</code> (Critical): Tools the agent cannot function without. Failures will cleanly abort with a clear error.</li>
<li><code>required: false</code> (Optional, Default): Auxiliary enhancements. Failures trigger circuit-breaking without affecting the main session.</li>
</ul>

<h3 class="relative group">2. Concurrent Probing with Isolated Timeouts
    <div id="2-concurrent-probing-with-isolated-timeouts" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-concurrent-probing-with-isolated-timeouts" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Using Tokio, each MCP server is probed in a dedicated asynchronous task (<code>tokio::spawn</code>) wrapped in a <code>tokio::time::timeout</code>:</p>
<ul>
<li>All servers handshake concurrently. Cold startup latency is bounded by the slowest individual server rather than their cumulative sum.</li>
<li>If an optional server fails to connect within 3 seconds or crashes, the error is caught and marked as <code>Degraded</code> rather than bubbling up.</li>
</ul>

<h3 class="relative group">3. Dynamic Tool Filtering and UI Awareness
    <div id="3-dynamic-tool-filtering-and-ui-awareness" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-dynamic-tool-filtering-and-ui-awareness" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Once all probing tasks settle:</p>
<ol>
<li>Successfully initialized tools are registered into the session context.</li>
<li>Tools from degraded servers are filtered out, preventing the model from hallucinating broken calls.</li>
<li>A lightweight notification informs the frontend which optional plugin failed, while keeping the main chat fully operational.</li>
</ol>
<hr>

<h2 class="relative group">3. Takeaway
    <div id="3-takeaway" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-takeaway" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>With startup isolation in place, even if network access is spotty or an MCP plugin config is broken, the agent’s core capabilities launch in sub-seconds.</p>
<p>Plugin systems that depend on external environments must design for failure. An issue in an optional feature should never break core tool availability.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Decoupling Heavy IO from UI Finalization: Eliminating Input Freezes in Desktop Agents</title>
      <link>https://blog.ferstar.org/en/posts/desktop-agent-streaming-lifecycle-reconciliation/</link>
      <pubDate>Mon, 31 Aug 2026 23:40:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/desktop-agent-streaming-lifecycle-reconciliation/</guid>
      <description>Desktop agents often lock the chat input in a frozen loading state after generation completes; decouple disk IO from UI lifecycles via bypass emission channels, optimistic unlocking, and periodic stale reconciliation; completely eliminate input freezes while maintaining deterministic state consistency.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>When building desktop or web-based agent clients, there is a recurring, annoying UX friction:</p>
<blockquote><p><strong>The model has finished streaming its last token on screen, but the input box stays disabled with a “Task in progress…” placeholder. You cannot focus the cursor. It takes several seconds to unlock, and switching away from the window mid-stream can sometimes freeze it in a loading state permanently.</strong></p>
</blockquote><p>This looks like a simple frontend state bug, but tracing through the stack reveals an issue of asynchronous disk persistence blocking the event dispatch pipeline.</p>
<pre class="not-prose mermaid">
flowchart TD
  subgraph Backend[Agent Host Process / Runtime]
    M[Receive Final Text Chunk] --> MC[Emit MessageComplete]
    MC --> P[Heavy Async Persistence: SQLite & Archiving]
    P --> AE[Emit AgentEnd Terminal Event]
  end

  subgraph Legacy[Legacy Serial Pattern]
    L1[Wait for Persistence to Finish] --> L2[Notify Frontend via Long IPC Queue]
    L2 --> L3[Clear isStreaming / Noticeable UI Freeze]
  end

  subgraph Optimized[Decoupled & Reconciled Pattern]
    MC -->|Optimistic: No Pending Tools| UI1[Unlock Input Box Instantly]
    AE -->|Bypass Fast Channel| UI1
    T[10s Periodic Stale Reconciliation] -.->|Handles Dropouts & Blur Edge Cases| UI1
  end
</pre>

<hr>

<h2 class="relative group">1. Why Does the Input Lock Up?
    <div id="1-why-does-the-input-lock-up" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-why-does-the-input-lock-up" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>From model output to disk storage, messages pass through three layers:</p>
<ol>
<li><strong>Sampling Runtime</strong>: Consumes the SSE stream, producing text deltas and <code>MessageComplete</code>.</li>
<li><strong>Host Process (Node.js / IPC Layer)</strong>: Manages cross-process communication, SQLite writes, image materialization, and JSONL archiving.</li>
<li><strong>Renderer Process (Frontend UI)</strong>: Manages reactive states like <code>isStreaming</code> and input enablement in React/Vue.</li>
</ol>
<p>Tracing the logs revealed three main bottlenecks:</p>

<h3 class="relative group">Culprit 1: Terminal Events Blocked Behind Heavy I/O
    <div id="culprit-1-terminal-events-blocked-behind-heavy-io" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#culprit-1-terminal-events-blocked-behind-heavy-io" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The legacy implementation waited for all disk writes in the turn to finish before sending <code>agent_end</code> to the UI:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// Legacy flow: Heavy I/O blocks terminal notifications
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">persistTurnToSqlite</span><span class="p">(</span><span class="nx">turnData</span><span class="p">);</span>      <span class="c1">// 50~200ms
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">materializeImagesToDisk</span><span class="p">(</span><span class="nx">images</span><span class="p">);</span>    <span class="c1">// 500ms~2s
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">appendSessionJsonl</span><span class="p">(</span><span class="nx">largePayload</span><span class="p">);</span>   <span class="c1">// 100~500ms
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Only now is the UI notified
</span></span></span><span class="line"><span class="cl"><span class="nx">emitToRenderer</span><span class="p">(</span><span class="s1">'agent_end'</span><span class="p">,</span> <span class="nx">session</span><span class="p">);</span></span></span></code></pre></div></div>
<p>While the user is already reading the final response, the host process is still grinding through disk writes. In large sessions, this easily introduces noticeable delays.</p>

<h3 class="relative group">Culprit 2: Head-of-Line Blocking in IPC Queues
    <div id="culprit-2-head-of-line-blocking-in-ipc-queues" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#culprit-2-head-of-line-blocking-in-ipc-queues" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>To preserve message ordering, events pass through an ordered queue (Emit Chain). If the model emits diagnostic logs during finalization, <code>agent_end</code> gets queued behind them.</p>

<h3 class="relative group">Culprit 3: Window Defocus Drops
    <div id="culprit-3-window-defocus-drops" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#culprit-3-window-defocus-drops" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>When a user switches windows mid-stream, Chromium throttles background timers. If a cross-process packet drops during that transition, the UI permanently misses <code>agent_end</code>, leaving the input locked.</p>
<hr>

<h2 class="relative group">2. The Solution
    <div id="2-the-solution" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-the-solution" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>We updated the IPC and frontend state management across three areas:</p>

<h3 class="relative group">1. Optimistic Unlocking
    <div id="1-optimistic-unlocking" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-optimistic-unlocking" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Input availability should not wait on disk writes. Once the frontend receives <code>MessageComplete</code> and verifies:</p>
<ul>
<li>The reply is a genuine terminal turn (<code>EndTurn</code>);</li>
<li>No background tools are currently executing;</li>
<li>The user has not requested a stop.</li>
</ul>
<p>It <strong>unlocks the input box immediately</strong>, dropping perceived recovery latency to zero.</p>

<h3 class="relative group">2. Bypass Channel for Terminal Events
    <div id="2-bypass-channel-for-terminal-events" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-bypass-channel-for-terminal-events" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Lifecycle events (<code>agent_end</code>, <code>error</code>, <code>cancelled</code>) have higher priority than regular stream deltas. We introduced a fast-track bypass in the IPC bridge:</p>
<ul>
<li>Terminal events skip the standard ordered queue and dispatch directly to the renderer.</li>
<li>Even if SQLite writes or archiving queues are backed up, the UI lifecycle updates without delay.</li>
</ul>

<h3 class="relative group">3. 10-Second Low-Overhead Reconciliation Loop
    <div id="3-10-second-low-overhead-reconciliation-loop" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-10-second-low-overhead-reconciliation-loop" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>To handle orphaned states from window blur or dropped packets, the frontend runs a 10-second reconciliation check:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">timer</span> <span class="o">=</span> <span class="nx">setInterval</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Check for sessions with no activity for >10s still marked as streaming
</span></span></span><span class="line"><span class="cl">    <span class="nx">reconcileStaleStreamingSessions</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span> <span class="mi">10</span><span class="nx">_000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">()</span> <span class="o">=></span> <span class="nx">clearInterval</span><span class="p">(</span><span class="nx">timer</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">},</span> <span class="p">[]);</span></span></span></code></pre></div></div>
<p>Any stuck sessions are idempotently cleaned up and synchronized.</p>
<hr>

<h2 class="relative group">3. Takeaway
    <div id="3-takeaway" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-takeaway" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>With these changes in place, the input box is ready the moment generation finishes, and focus freezes from window switching are gone.</p>
<p>In rich-client apps, keep user interactions optimistic and fast, run heavy persistence asynchronously in the background, and use periodic reconciliation to ensure eventual consistency.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Beyond Brute-Force Aborts: Graded Loop Governance for Agents (Warn → Block → Halt)</title>
      <link>https://blog.ferstar.org/en/posts/agent-graded-loop-guard-warn-block-halt/</link>
      <pubDate>Mon, 31 Aug 2026 23:39:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/agent-graded-loop-guard-warn-block-halt/</guid>
      <description>Agents tackling complex tasks often fall into repetitive tool-calling loops that inflate context and exhaust budgets; design a graded guardrail mechanism based on canonical argument hashing and side-effect classification; achieve an autonomous recovery loop spanning gentle steering, synthetic interception, and deterministic halt.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>When running complex tasks, one of the worst states an agent can enter is a repetitive execution loop.</p>
<p>For example, if a regex finds nothing, the model might dispatch the exact same <code>grep_search</code> three or four times in a row. Or if a file does not exist, it repeatedly calls <code>view_file</code> on the same path. Each call returns an identical empty result or error, yet the model stubbornly keeps trying.</p>
<p>Legacy approaches to handling loops are usually blunt:</p>
<ol>
<li><strong>Let it run until max_turns exhausts</strong>: Wasting dozens of API calls and crashing with a generic “maximum turns exceeded” error.</li>
<li><strong>Throw an exception immediately upon repetition</strong>: Aborting the task right away. But all the accumulated investigation context and progress vanish with it.</li>
</ol>
<p>Neither approach works well. We added a Graded Loop Guard to our runtime using a three-tier escalation ladder.</p>
<pre class="not-prose mermaid">
flowchart TD
  subgraph Ingestion[Tool Call Ingestion]
    A[Receive Model Tool Call] --> B[Compute Canonical Args Hash]
    B --> C[Classify Side-Effects: Read-Only vs Mutating]
  end

  subgraph Ladder[Graded Escalation Ladder]
    C --> D{Sequential Duplicate Count}
    D -->|1st Duplicate Count=2| E[Warn: Execute Tool & Append Steering Guidance]
    D -->|2nd Duplicate Count=3| F[Block: Intercept Execution & Return Synthetic Error]
    D -->|3rd Duplicate Count=4| G[Halt: Abort Turn & Expose repeated_tool_calls]
  end

  subgraph Outcome[Outcome & Recovery]
    E --> H[Model Reads Hint & Self-Corrects]
    F --> I[Prevent Wasted IO & Force Strategy Shift]
    G --> J[Preserve Context & Provide Clear Diagnostics]
  end

  Ingestion --> Ladder
</pre>

<hr>

<h2 class="relative group">1. Why Brute-Force Aborts Fail
    <div id="1-why-brute-force-aborts-fail" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-why-brute-force-aborts-fail" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Determining whether a model is genuinely looping requires a few distinctions:</p>
<ul>
<li><strong>Read-Only vs. Mutating Tools</strong>: Repeatedly inspecting the same file with read-only tools (<code>grep</code>, <code>view_file</code>) is often benign, so tolerance can be higher. But repeating mutating actions (<code>write_to_file</code> or running write commands) with identical arguments is dangerous.</li>
<li><strong>Models Can Self-Correct</strong>: Often, the model is simply stuck in a temporary rut. If you explicitly remind it in the tool result (<em>“You have run this exact query twice with zero results; please adjust your approach”</em>), modern reasoning models usually pivot autonomously in the next turn.</li>
</ul>
<p>The guardrail’s design logic is straightforward: <strong>guide first, block physically second, halt last.</strong></p>
<hr>

<h2 class="relative group">2. The Three-Tier Escalation Ladder
    <div id="2-the-three-tier-escalation-ladder" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-the-three-tier-escalation-ladder" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The guard checks invocations in the <code>before_tool_call</code> phase of the main loop:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="cp">#[derive(Debug, Clone, Copy, PartialEq, Eq)]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">pub</span><span class="w"> </span><span class="k">enum</span> <span class="nc">LoopGuardAction</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="sd">/// Allow: Initial or normal execution
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Allow</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="sd">/// Warn: Inject guidance into tool result, execute normally
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Warn</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="sd">/// Block: Prevent physical execution, return synthetic error
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Block</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="sd">/// Halt: Abort active turn
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Halt</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">Tier 1: Warn (Steering Guidance Injection)
    <div id="tier-1-warn-steering-guidance-injection" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#tier-1-warn-steering-guidance-injection" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>When the same tool with identical normalized arguments appears for the second time in a task:</p>
<ul>
<li>The tool executes normally.</li>
<li>When the tool finishes, the runtime appends an automated notice to the end of the <code>ToolResult</code>:
<blockquote><p><code>[System Notice] You have executed this tool repeatedly with identical arguments without progress. Do not repeat identical calls. Adjust your search pattern, change the target path, or switch tools.</code></p>
</blockquote></li>
<li>Most models immediately adjust their strategy on the next turn.</li>
</ul>

<h3 class="relative group">Tier 2: Block (Synthetic Interception)
    <div id="tier-2-block-synthetic-interception" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#tier-2-block-synthetic-interception" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>If the model ignores the warning and calls the exact same tool a third time:</p>
<ul>
<li>The guard short-circuits the call, skipping physical file I/O or command execution entirely.</li>
<li>A structured error (<code>ToolResultContent::Error</code>) is synthesized at the protocol layer, informing the model that repeated execution was blocked.</li>
<li>LLM protocols mandate that every <code>tool_use</code> must have an accompanying <code>tool_result</code>. Returning a synthetic error preserves protocol parity while preventing wasted computation.</li>
</ul>

<h3 class="relative group">Tier 3: Halt (Safe Turn Abort)
    <div id="tier-3-halt-safe-turn-abort" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#tier-3-halt-safe-turn-abort" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>If duplicate calls reach four (<code>HALT_AFTER = 4</code>), the model is stuck:</p>
<ul>
<li>The runtime ends the current turn and triggers <code>RepeatedToolCalls</code>.</li>
<li>The UI and logs explicitly identify the offending tool name and arguments.</li>
<li>Prior session history remains intact on disk, allowing the user to guide the agent with a follow-up prompt without re-running from scratch.</li>
</ul>
<hr>

<h2 class="relative group">3. Canonical Argument Matching
    <div id="3-canonical-argument-matching" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-canonical-argument-matching" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>To prevent false negatives from varying JSON key order, the guard sorts JSON object keys recursively before computing <code>canonical_args_hash</code>. This ensures <code>{"a": 1, "b": 2}</code> and <code>{"b": 2, "a": 1}</code> yield identical fingerprints.</p>
<p>Tools are also categorized as <code>ReadOnly</code> or <code>Mutating</code>, applying tighter thresholds to state-modifying actions.</p>
<hr>

<h2 class="relative group">4. Takeaway
    <div id="4-takeaway" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-takeaway" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>In real workloads, most edge-case loops resolve autonomously during the <code>Warn</code> tier.</p>
<p>Replacing abrupt aborts with a progressive ladder of warnings, synthetic blocks, and clean halts preserves token budgets while meaningfully boosting task completion rates.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>When Models Leak Tool Calls as Text: Stream Recovery with a Cross-Chunk DSML State Machine</title>
      <link>https://blog.ferstar.org/en/posts/agent-dsml-tool-call-streaming-recovery/</link>
      <pubDate>Mon, 31 Aug 2026 23:38:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/agent-dsml-tool-call-streaming-recovery/</guid>
      <description>Certain models occasionally leak tool invocations as raw full-width DSML text rather than structured payloads, breaking agent execution; design a provider-agnostic, cross-chunk streaming state machine with defensive protocol recovery; completely prevent text leakage while transparently restoring tool calls to active execution.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>When integrating diverse models for coding agents, transport quirks are bound to happen.</p>
<p>One recurring issue is when a model, instead of returning structured function calls via the standard <code>tool_calls</code> payload, dumps raw XML markup directly into the text stream (<code>content</code> or <code>output_text.delta</code>).</p>
<p>In certain reasoning models (such as DeepSeek derivatives or proxied gateways), it often looks like this:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">＜ＤＳＭＬ＜tool_calls＞
</span></span><span class="line"><span class="cl">＜ＤＳＭＬ＜invoke name="read_file"＞
</span></span><span class="line"><span class="cl">＜ＤＳＭＬ＜parameter name="path"＞"src/main.rs"＜／ＤＳＭＬ＜／parameter＞
</span></span><span class="line"><span class="cl">＜／ＤＳＭＬ＜／invoke＞
</span></span><span class="line"><span class="cl">＜／ＤＳＭＬ＜／tool_calls＞</span></span></code></pre></div></div>
<p>If the transport layer simply passes these tokens through to the UI as plain text:</p>
<ol>
<li>The agent loop receives zero <code>tool_call</code> events, stalling the task.</li>
<li>The user’s screen gets littered with unparsed full-width XML tags.</li>
</ol>
<p>To handle this cleanly in our runtime, we built a cross-chunk streaming DSML recovery state machine.</p>
<pre class="not-prose mermaid">
flowchart TD
  subgraph Ingestion[Streaming Input Token Chunks]
    A[Chunk 1: Protocol Prefix] --> B[Chunk 2: invoke name=read_file]
    B --> C[Chunk 3: parameter name=path]
    C --> D[Chunk 4: Protocol Closing Tag]
  end

  subgraph StateMachine[DSML Streaming State Machine]
    S1[Detect Prefix: ＜ＤＳＭＬ＜] --> S2{Is Prompt Example?}
    S2 -->|Yes| S3[Disable Recovery / Stream as Text]
    S2 -->|No| S4[Capture Mode / Hold Text Output]
    S4 --> S5[Buffer Chunks & Assemble Tags]
    S5 --> S6{Is Markup Valid & Closed?}
    S6 -->|No or Over Limit| S7[Fail-Closed / Emit Safe Error]
    S6 -->|Yes| S8[Extract Tool Name & Parameter Pairs]
  end

  subgraph Dispatch[Protocol Conversion & Dispatch]
    S8 --> E1[Validate against Tool Schema & Deserialize]
    E1 --> E2[Synthesize ToolCall & ToolUse Events]
    E2 --> E3[Agent Loop Executes Real Tool]
  end

  Ingestion --> StateMachine
</pre>

<hr>

<h2 class="relative group">1. Where the Complexity Lies
    <div id="1-where-the-complexity-lies" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-where-the-complexity-lies" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>If you receive a single, complete HTTP response body, extracting the tags via regex or an XML parser is straightforward. But agents require low-latency streaming text, which introduces several constraints:</p>
<ol>
<li><strong>Chunk Fragmentation</strong>: Output arrives token-by-token. A tag like <code>＜ＤＳＭＬ＜invoke</code> might arrive fragmented as <code>["＜", "ＤＳ", "ＭＬ＜in", "voke"]</code> across four separate network packets. Single-chunk regex matching is ineffective.</li>
<li><strong>Hold Buffers</strong>: When receiving a partial prefix (like a standalone <code>＜</code>), we cannot stream it immediately to the client (in case it turns out to be markup). But we also cannot hold it indefinitely; normal text must flush immediately once verified.</li>
<li><strong>User Prompt Examples</strong>: If a user is explicitly discussing DSML syntax (e.g., <em>“What does ＜ＤＳＭＬ＜ mean?”</em>), the state machine must recognize this and disable recovery, rather than attempting to execute quoted examples as real system commands.</li>
<li><strong>Native vs. DSML Conflicts</strong>: If a model returns both native structured tool calls and raw DSML text in the same turn, we fail closed to prevent duplicate executions.</li>
</ol>
<hr>

<h2 class="relative group">2. State Machine Design & Intermediate Representation
    <div id="2-state-machine-design--intermediate-representation" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-state-machine-design--intermediate-representation" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>To support multiple providers (OpenAI-compatible endpoints, Responses APIs), the state machine operates on a decoupled Intermediate Representation:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="k">pub</span><span class="w"> </span><span class="k">enum</span> <span class="nc">DsmlOutcome</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="sd">/// Confirmed user-visible text, released for frontend streaming
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">Text</span><span class="p">(</span><span class="nb">String</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="sd">/// Successfully captured and parsed complete tool invocations
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ToolCalls</span><span class="p">(</span><span class="nb">Vec</span><span class="o"><</span><span class="n">DsmlToolCall</span><span class="o">></span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">pub</span><span class="w"> </span><span class="k">struct</span> <span class="nc">DsmlToolCall</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">pub</span><span class="w"> </span><span class="n">id</span>: <span class="nb">String</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">pub</span><span class="w"> </span><span class="n">name</span>: <span class="nb">String</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">pub</span><span class="w"> </span><span class="n">arguments</span>: <span class="nb">String</span><span class="p">,</span><span class="w"> </span><span class="c1">// Normalized standard JSON string
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>The provider passes incoming text chunks into the state machine:</p>
<ul>
<li>It maintains an internal <code>capture_buffer</code>.</li>
<li>Once <code>＜ＤＳＭＬ＜</code> is detected, it switches to capture mode, pausing downstream <code>TextDelta</code> emissions.</li>
<li>A <code>DSML_CAPTURE_LIMIT</code> (256KB) guards against memory exhaustion from malformed output.</li>
</ul>
<hr>

<h2 class="relative group">3. Schema Validation & Event Synthesis
    <div id="3-schema-validation--event-synthesis" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-schema-validation--event-synthesis" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>When closed tags are parsed, <code><invoke></code> and <code><parameter></code> nodes are extracted:</p>
<ol>
<li>Verify the tool name exists in the current registry.</li>
<li>Parse non-string parameters into valid JSON values.</li>
<li>Validate against the tool’s registered JSON Schema.</li>
<li>Synthesize native <code>ToolCallStart</code>, <code>ToolInputDelta</code>, and <code>ToolUse</code> events.</li>
</ol>
<p>From the agent loop’s perspective, this is indistinguishable from standard provider tool calls, routing directly into normal tool execution.</p>
<hr>

<h2 class="relative group">4. Takeaway
    <div id="4-takeaway" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-takeaway" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>When building agent runtimes against varied model endpoints, output formatting anomalies are inevitable. Absorbing these quirks in the transport adapter layer keeps higher-level agent state machines clean and dependable.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Moving Context Compaction Off the Critical Path: Two-Pass Prefire and Recovery Hints for Long-Running Agents</title>
      <link>https://blog.ferstar.org/en/posts/agent-context-compaction-two-pass-prefire/</link>
      <pubDate>Mon, 31 Aug 2026 23:20:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/agent-context-compaction-two-pass-prefire/</guid>
      <description>In-flight context compaction causes noticeable UI pauses and drops execution details in long agent sessions; introduce Two-Pass Prefire background summarization with append-only JSONL recovery hints, alongside prune-first retries and terminal checkpoints; achieve near-zero perceived compaction latency while retaining full historical detail retrieval.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>When running long agent tasks, context grows fast.</p>
<p>Refactoring code, checking logs, running test suites—after dozens of turns, tokens quickly approach their limit. The simplest legacy approach was single-pass compaction: when limits are crossed, stop the main loop, send the entire message history to an LLM for a global summary, and swap out old messages.</p>
<p>In practice, several problems surfaced quickly:</p>
<ol>
<li><strong>Main-loop pauses</strong>: Sending tens of kilobytes of history for an LLM summary often takes seconds or tens of seconds. The client input box just sits in a loading state.</li>
<li><strong>Lost execution details</strong>: Once condensed into prose, exact error line numbers, file paths, and stray arguments vanish. When subsequent steps need them, the model either re-queries from scratch or hallucinates.</li>
<li><strong>Broken KV caching</strong>: Cramming the summary into the System Prompt mutates the prompt prefix on provider servers, busting Prompt Caching completely and driving up both TTFT and API costs.</li>
<li><strong>Wasted summaries when pruning would suffice</strong>: Often, context blows up simply because a tool dumped a massive <code>git diff</code> or build log. Truncating old tool outputs reclaims tens of thousands of tokens without needing an LLM summary at all.</li>
</ol>
<p>I overhauled our agent runtime’s context management over the past three days. Here is what changed.</p>
<pre class="not-prose mermaid">
flowchart TD
  subgraph Prefire[Phase 1: Pass 1 Background Prefire]
    A[Tokens hit 90% threshold] --> B[Slice 95% prefix history]
    B --> C[Async background LLM summary]
    C --> D[Cache NOTE1 note and fingerprint]
  end

  subgraph Foreground[Foreground Main Loop]
    U[Normal turns and tool calls] --> E[Generate new turns]
  end

  subgraph Compaction[Phase 2: Pass 2 Compaction]
    F[Tokens cross 100% threshold] --> G{Fingerprint matches?}
    G -->|Match| H[Merge NOTE1 with tail delta]
    G -->|Drift or Failure| I[Fallback to full single-pass summary]
    H --> J[Generate final summary]
    I --> J
    J --> K[Inject standalone summary turn & JSONL pointer]
  end

  Prefire -.->|Runs in background| Foreground
  Foreground --> F
</pre>

<hr>

<h2 class="relative group">1. Two-Pass Prefire: Moving Heavy Summaries to the Background
    <div id="1-two-pass-prefire-moving-heavy-summaries-to-the-background" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-two-pass-prefire-moving-heavy-summaries-to-the-background" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>To keep the UI responsive, the heaviest work must happen in the background before the hard limit is hit.</p>

<h3 class="relative group">Phase 1: Prefix Prefire (Pass 1)
    <div id="phase-1-prefix-prefire-pass-1" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#phase-1-prefix-prefire-pass-1" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>We added a safety margin (<code>prefire_margin_tokens</code>, defaulting to a 10% buffer).</p>
<p>When context usage reaches 90% of the threshold, the main loop keeps running while spawning a background task:</p>
<ul>
<li><strong>Slice the prefix</strong>: Take the oldest 95% of messages. Slicing must strictly protect <code>tool_use</code> and <code>tool_result</code> boundaries without severing unclosed invocations.</li>
<li><strong>Generate NOTE1</strong>: The prefix is summarized into a structured note (<code>NOTE1</code>), and a hash fingerprint of that prefix is stored in cache.</li>
<li>The foreground streams text normally without interruption.</li>
</ul>

<h3 class="relative group">Phase 2: Incremental Compaction (Pass 2)
    <div id="phase-2-incremental-compaction-pass-2" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#phase-2-incremental-compaction-pass-2" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>When subsequent turns push total tokens past the 100% threshold, formal compaction kicks in:</p>
<ul>
<li>Verify the fingerprint. If the prefix hasn’t changed and the background task finished, grab the cached <code>NOTE1</code>.</li>
<li>Instead of feeding the whole history to the model, we only send <code>NOTE1</code> plus the few tail messages produced after Pass 1.</li>
<li>If the fingerprint drifted or Pass 1 failed, it cleanly falls back to standard single-pass compaction.</li>
</ul>
<p>This cuts the tokens sent during active compaction by over 80%, dropping main-thread wait times to milliseconds.</p>
<hr>

<h2 class="relative group">2. Compaction Is Not Deletion: Recovery Hints Pointing to Local JSONL
    <div id="2-compaction-is-not-deletion-recovery-hints-pointing-to-local-jsonl" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-compaction-is-not-deletion-recovery-hints-pointing-to-local-jsonl" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Compacting history shouldn’t mean destroying raw details.</p>
<ol>
<li><strong>Local append-only source of truth</strong>: All raw messages, arguments, and full outputs are continuously written to a local session JSONL file and are never deleted.</li>
<li><strong>Recovery pointers</strong>: The <code><compaction-summary></code> block automatically includes a <code><recovery_hint></code> tag containing the session’s absolute JSONL path:
<blockquote><p>History has been summarized. Full raw records remain in <code>session.jsonl</code>. If subsequent steps require exact line numbers, error traces, or command outputs, inspect this file directly with read tools.</p>
</blockquote></li>
<li><strong>Leave the System Prompt alone</strong>: Summaries are injected as standalone conversation turns. The System Prompt stays frozen, keeping server-side Prompt Caching hit rates high.</li>
</ol>
<hr>

<h2 class="relative group">3. Prune First Before Calling an LLM Summary
    <div id="3-prune-first-before-calling-an-llm-summary" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-prune-first-before-calling-an-llm-summary" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Often, invoking an LLM for summarization is unnecessary.</p>
<p>Before running full compaction, we execute <code>trim_old_tool_results</code> to truncate verbose outputs from older turns into short previews. If pruning frees enough tokens, we set <code>context_changed = true</code> and retry immediately, saving an expensive summary call.</p>
<p>We also resolved an edge case: when a turn’s final assistant reply (<code>EndTurn</code>) pushes context over the threshold, compaction used to wait until the user’s next prompt. Now, the runtime compacts and persists checkpoints immediately between <code>MessageComplete</code> and <code>TurnEnd</code>, ensuring cold reboots wake up to clean context.</p>
<hr>

<h2 class="relative group">4. Real-World Telemetry
    <div id="4-real-world-telemetry" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-real-world-telemetry" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Testing over a continuous 47-hour session with 1,000 requests and 490 million input tokens yielded clear metrics:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Metric</th>
          <th style="text-align: left">Pre-fix (964 turns)</th>
          <th style="text-align: left">Post-fix (36 turns)</th>
          <th style="text-align: left">Change</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Avg. Input / Turn</strong></td>
          <td style="text-align: left">496,806 tokens</td>
          <td style="text-align: left">299,625 tokens</td>
          <td style="text-align: left"><strong>↓ 40%</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Context Shape</strong></td>
          <td style="text-align: left">1.75M full replay (inflated)</td>
          <td style="text-align: left">~250K baseline / 280K steady</td>
          <td style="text-align: left"><strong>Bounded</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Prompt Cache Hit Rate</strong></td>
          <td style="text-align: left">99.11%</td>
          <td style="text-align: left">96.91% (includes cold restarts)</td>
          <td style="text-align: left"><strong>99%+ sustained</strong></td>
      </tr>
  </tbody>
</table>
<p>Average per-turn tokens dropped by 40%, and context stabilized around 280K instead of ballooning uncontrollably. Beyond a minor ~20K cache miss right after compaction restarts, subsequent turns sustained 99% to 100% prompt cache hit rates.</p>
<p>Keeping long-running agents reliable comes down to getting these state transitions and caching boundaries right.</p>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
