How the Circuit Breaker in Munder Difflin Prevents Agent Runaway Behavior

The Circuit Breaker in Munder Difflin is a safety guard that monitors every active agent on each heartbeat tick and enforces a four-level escalation ladder—healthy, steering, constrained, and stopped—to prevent unbounded token consumption, tool looping, and stale progress.

Munder Difflin implements this circuit breaker pattern to enforce Policy Lane A #6.6b, ensuring that autonomous agents cannot drain budgets or spin infinitely on unproductive tasks. According to the source code in src/main/breaker.ts, the system evaluates multiple risk factors on every tick and escalates or recovers agent privilege levels based on observed behavior.

Architectural Overview of the Circuit Breaker

The implementation separates configuration, state tracking, and evaluation logic into distinct components across the codebase.

Configuration and Thresholds

The CircuitBreakerConfig interface in src/main/config.ts (lines 42-53) defines the operational parameters:

  • .enabled: Toggles the breaker system on or off
  • .hardStop: Determines whether the stopped level terminates the agent
  • .repeatedToolLimit: Threshold for identical tool call loops (default: 8)
  • .errorStormLimit: Consecutive API error threshold (default: 5)
  • .tokenVelocityPerMin: Output tokens per minute limit (default: 60,000)
  • .costCapUsd and .costCapTokens: Floor-wide spending limits
  • .agentTokenCaps: Per-agent token limits

Default configurations are conservative, with hardStop disabled and small repeat limits to catch issues early without immediate termination.

The Four-Level Escalation Ladder

As defined in src/main/breaker.ts (lines 28-33), agents traverse four distinct states:

  1. healthy: Normal operation with no restrictions
  2. steering: Agent receives corrective messages to change behavior
  3. constrained: Resource throttling applied (reduced token generation)
  4. stopped: Agent killed and session archived (only if hardStop is enabled)

The AgentBreakerState interface (lines 89-108) tracks the current level, trip reason, usage samples, repeat counters, error counters, and a debounce counter for no-progress detection.

State Management and Event Hooks

The CircuitBreaker class maintains internal state through three event-recording methods:

  • recordToolUse(agentId, toolName, params): Tracks distinct tool invocations
  • recordError(agentId): Increments consecutive error counters
  • recordCompactStart/End(agentId): Manages compaction exemption windows

These methods feed into the evaluation cycle without triggering immediate side effects, keeping the breaker pure and testable.

How the Circuit Breaker Evaluates Agents

The core logic resides in the evaluate method (src/main/breaker.ts, lines 94-145), which processes BreakerInput containing the agent ID, cumulative usage sample, and a progress boolean indicating recent file modification activity.

The Seven Trip Conditions

The evaluator checks these conditions in strict order, returning { tripping: true, reason } on the first match:

  1. Repeated Identical Tool Calls: Triggered when repeatCount >= repeatedToolLimit (default 8), indicating a loop such as "8× identical tool call (write)"
  2. API Error Storm: Fires when errorCount >= errorStormLimit (default 5), signaling "5 consecutive api errors/retries"
  3. Per-Agent Token Cap: Activates when tokensOf(sample) > agentTokenCaps[agentId], e.g., "1,200,000 over the agent cap of 1,000,000"
  4. Floor-Wide Cost Cap: Checks if total USD exceeds costCapUsd and this agent is the top spender
  5. Floor-Wide Token Cap: Validates total tokens against costCapTokens with top-spender targeting
  6. Token-Velocity Spike: Trips when Δoutput/Δminutes exceeds tokenVelocityPerMin (default 60,000), catching "token velocity 120,000/min > 60,000/min"
  7. No-Progress Condition: Requires no file-mtime progress and no distinct tool call within 5 minutes for ≥2 consecutive beats, flagging "generating tokens without coordinating (stale log/files)"

Escalation and Recovery Logic

The tick method (lines 162-180) orchestrates state transitions:

  • Escalation: If evaluate returns tripping: true, the level increments by one step (capped at stopped when hardStop is true)
  • Recovery: If no trip occurs, the level decrements toward healthy
  • Action Determination: Returns BreakerDecision with action set to steer, constrain, or stop only when escalating; none otherwise

The heartbeat loop in src/main/index.ts consumes these decisions to send steering messages, apply throttles, or kill the PTY. It also emits BreakerState on the control:breakerState channel to synchronize the dashboard UI.

Implementing the Circuit Breaker in Practice

The following TypeScript example demonstrates how to instantiate the breaker, record external events, and process heartbeat ticks:

import { CircuitBreaker } from './breaker';
import type { CircuitBreakerConfig } from './config';
import type { AgentUsageSample } from './usage';

// Initialize with configuration getter
const breaker = new CircuitBreaker(() => ({
  enabled: true,
  hardStop: false,
  repeatedToolLimit: 8,
  errorStormLimit: 5,
  tokenVelocityPerMin: 60_000,
  costCapUsd: 100,
  costCapTokens: 2_000_000,
  agentTokenCaps: { 'agent-1': 1_000_000 }
}));

// Record asynchronous events as they occur
breaker.recordToolUse('agent-1', 'write', { path: '/tmp/file.txt' });
breaker.recordError('agent-1');

// Process heartbeat tick (mirrors src/main/index.ts implementation)
function heartbeatTick(nowMs: number) {
  const inputs = [
    {
      agentId: 'agent-1',
      sample: getUsageSample('agent-1'), // { input, output, cacheRead, cacheCreation, usd, ts }
      progressing: checkFileMtimeProgress('agent-1') // boolean
    }
  ];

  const decisions = breaker.tick(inputs, nowMs);
  
  decisions.forEach(({ state, action, changed }) => {
    if (changed) {
      console.log(`Breaker ${state.agentId} → ${state.level}: ${state.reason}`);
    }
    
    switch (action) {
      case 'steer':
        sendSteerMessage(state.agentId, state.reason);
        break;
      case 'constrain':
        applyThrottle(state.agentId);
        break;
      case 'stop':
        killAgent(state.agentId);
        archiveSession(state.agentId);
        break;
    }
  });
}

This pattern follows the actual implementation in src/main/index.ts, where the breaker remains pure and the caller handles all side effects.

Summary

  • The Circuit Breaker in Munder Difflin enforces Policy Lane A #6.6b through a four-level escalation system defined in src/main/breaker.ts.
  • Seven distinct trip conditions detect runaway behavior including tool loops, error storms, token velocity spikes, and stale progress.
  • Pure evaluation logic separates state tracking from enforcement, with the heartbeat loop in src/main/index.ts handling steering, throttling, and termination.
  • Configuration lives in src/main/config.ts with conservative defaults for repeatedToolLimit (8) and errorStormLimit (5).
  • Comprehensive testing is available in test/breaker.test.cjs covering trip scenarios and recovery paths.

Frequently Asked Questions

What triggers the Circuit Breaker to trip?

The breaker trips on one of seven conditions evaluated in order: repeated identical tool calls exceeding the configured limit, consecutive API error storms, per-agent token caps, floor-wide cost or token caps when the agent is the top spender, token-velocity spikes exceeding the per-minute threshold, or a no-progress condition where the agent generates tokens without file modifications or distinct tool usage for multiple beats.

How does the Circuit Breaker recover to healthy status?

Recovery occurs automatically during the tick cycle when evaluate returns tripping: false. The agent's level decrements by one step toward healthy (e.g., from constrained to steering, or steering to healthy). This gradual de-escalation prevents flapping while allowing agents to resume normal operations once metrics stabilize.

What is the difference between constrain and stop actions?

The constrain action triggers resource throttling that limits token generation without terminating the session, while stop kills the PTY and archives the session. The stop action only occurs when the agent reaches the stopped level and hardStop is enabled in the configuration; otherwise, agents remain in lower escalation levels indefinitely.

Where is the Circuit Breaker configuration defined?

Configuration resides in src/main/config.ts through the CircuitBreakerConfig interface, which specifies thresholds for tool repetition, error storms, token velocity, and spending caps. The CircuitBreaker class accepts a getter function returning this config, allowing dynamic updates without restarting the breaker instance.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →