# How the Circuit Breaker in Munder Difflin Prevents Agent Runaway Behavior

> Discover the Circuit Breaker in Munder Difflin. This safety guard monitors agents, preventing runaway behavior with a four-level escalation to control token consumption and ensure progress.

- Repository: [Chaitanya Giri/munder-difflin](https://github.com/chaitanyagiri/munder-difflin)
- Tags: internals
- Published: 2026-08-22

---

**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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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:

```typescript
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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/src/main/index.ts) handling steering, throttling, and termination.
- **Configuration** lives in [`src/main/config.ts`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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`](https://github.com/chaitanyagiri/munder-difflin/blob/main/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.