How the breaker.ts Steer → Constrain → Stop Ladder Works in Munder Difflin

The breaker.ts steer → constrain → stop ladder moves agents one level per heartbeat—from healthy to steering, then constrained, and finally stopped—based on evaluated trip conditions, while recovering one level when no trip occurs.

The circuit breaker in chaitanyagiri/munder-difflin safeguards the system against runaway agent behavior by evaluating every agent during each periodic beat. In src/main/breaker.ts, the steer → constrain → stop ladder enforces increasing restrictions through ordered state transitions defined by the LEVELS array and the actionFor helper, allowing recovery when conditions normalize.

The Four-Level Escalation Hierarchy

At the core of breaker.ts is the ordered LEVELS array that pins the exact sequence an agent must follow.

const LEVELS: BreakerLevel[] = ['healthy', 'steering', 'constrained', 'stopped'];
const rank = (l: BreakerLevel): number => LEVELS.indexOf(l);

The ladder never skips ranks. An agent climbs from healthy to steering on the first trip, to constrained on a second consecutive trip, and to stopped on a third. A healthy beat always drops the agent down by exactly one step toward healthy.

Mapping States to Enforcement Actions

Only active levels produce an enforcement verb. The actionFor helper at line 61 of src/main/breaker.ts translates rank into behavior.

const actionFor = (l: BreakerLevel): BreakerAction =>
  l === 'steering' ? 'steer' : l === 'constrained' ? 'constrain' : l === 'stopped' ? 'stop' : 'none';

When the state is healthy, actionFor returns none. For every other level it returns the matching verb, which the heartbeat loop uses to trigger corrective messages or termination.

How tick Evaluates and Transitions Agents

The tick method drives the ladder. It accepts an array of BreakerInput objects—one per agent—and returns a BreakerDecision for each.

const decisions = breaker.tick(inputs, Date.now());

Inside tick, the method first checks the global cfg object pulled from src/main/config.ts. If the circuit breaker is disabled, every agent is forced back to healthy. When enabled, tick identifies top-spender agents for cost-cap and token-cap comparisons before proceeding to individual evaluation.

Trip Logic in evaluate

For each agent, tick calls evaluate (line 84), a pure function that inspects per-agent counters and usage samples. If any trip condition is met, evaluate signals that the agent should climb the ladder.

The tick method then compares the current level with the evaluation result. On a trip, the target level becomes the next higher rank, clamped to the configured ceiling. On a clean bill of health, the level decreases by one rank.

Escalation, Recovery, and Output Flags

The ladder moves at most one level per heartbeat. This prevents flickering and guarantees safe escalation. The returned BreakerDecision includes a changed flag when any shift occurs, and an action field only when the level increased (escalated). The new BreakerState is emitted on the control:breakerState channel so dashboards can remain synchronized.

Trip Conditions That Drive the Ladder

The evaluate function monitors four families of signals to decide whether an agent should escalate.

Repeated Identical Tool Calls

When repeatCount exceeds repeatedToolLimit, the breaker treats the agent as stuck in a loop. This is one of the fastest ways to trigger an initial steer action.

API-Error Storms

Consecutive API errors beyond errorStormLimit indicate a malfunctioning integration or malformed prompt. Each error increments an internal counter that evaluate inspects during the beat.

Cost and Token Caps

The breaker calculates floor-wide totals and flags the top-spender agent when cumulative cost or token usage crosses configured ceilings. These checks correspond to the cost-cap logic (lines 30–38) and token-cap logic (lines 40–48) inside src/main/breaker.ts.

Token-Velocity Spikes

A rapid increase in output tokens per minute is detected by comparing the current AgentUsageSample from src/main/usage.ts with the previous one. The tokenVelocityPerMin threshold catches agents that suddenly begin generating excessive output.

No-Progress Detection

If an agent produces tokens without file-system progress or a distinct tool call within a five-minute window, a debounce counter (NO_PROGRESS_BEATS) eventually triggers a trip. This catches agents that appear busy but accomplish nothing.

Hard Stop and the Ladder Ceiling

The CircuitBreakerConfig in src/main/config.ts exposes a hardStop flag that determines the terminal ceiling. When enabled, the ladder can reach stopped and the agent is killed or archived. When disabled, escalation caps at constrained, keeping the agent alive under heavy restriction. This logic is applied inside tick at the escalation boundary.

Implementing the Ladder in Practice

Heartbeat Loop Integration

The following pattern shows how to run the breaker inside a periodic beat and act only on escalations.

import { CircuitBreaker } from './breaker';

// Provide a function returning the current configuration (could come from a
// config file or a UI control).
const getConfig = () => ({
  enabled: true,
  hardStop: false,
  repeatedToolLimit: 8,
  errorStormLimit: 5,
  tokenVelocityPerMin: 60_000,
  // optional caps…
});

const breaker = new CircuitBreaker(getConfig);

// Inside the periodic “beat”:
const inputs: BreakerInput[] = agents.map(a => ({
  agentId: a.id,
  sample: a.usageSample,      // cumulative usage or null
  progressing: a.fileMtimeProgress // true if recent file change
}));

const decisions = breaker.tick(inputs, Date.now());

decisions.forEach(d => {
  // Emit state for monitoring UI
  publish('control:breakerState', d.state);
  // Perform enforcement only when an escalation action is present
  if (d.action !== 'none') enforceAction(d.agentId, d.action);
});

Recording Events from the Hook Server

External callers such as src/main/hookServer.ts feed the breaker through dedicated record methods so that evaluate has fresh data on each tick.

// When a tool finishes:
breaker.recordToolUse(agentId, toolName, toolInput);

// When an api_error occurs:
breaker.recordError(agentId);

// When a compaction starts / ends (to avoid false‑positive velocity trips):
breaker.recordCompactStart(agentId);
breaker.recordCompactEnd(agentId);

These calls update the per-agent counters inspected by the trip logic in src/main/breaker.ts.

Summary

  • The steer → constrain → stop ladder in src/main/breaker.ts uses four ordered levels: healthy, steering, constrained, and stopped.
  • The LEVELS array and actionFor helper map each state to none, steer, constrain, or stop.
  • The tick method evaluates every agent once per heartbeat, allowing at most one level of escalation or recovery per beat.
  • Trip conditions include repeated tool calls, error storms, cost caps, token velocity, and no-progress detection.
  • The hardStop configuration flag in src/main/config.ts determines whether the ladder ceiling is constrained or stopped.

Frequently Asked Questions

How does the breaker.ts steer constrain stop ladder move an agent up?

The ladder advances one level per trip condition detected during tick. If evaluate finds a tripping signal while the agent is healthy, it moves to steering; a second trip moves it to constrained; a third trip moves it to stopped when hardStop is enabled. The escalation logic inside tick enforces this single-step progression according to the LEVELS ranking.

What prevents the circuit breaker from jumping straight to stopped?

The escalation logic inside tick limits movement to a single rank per heartbeat. Because the LEVELS array enforces a strict sequence, an agent must pass through steering and constrained before reaching stopped, preventing instantaneous termination from a healthy state.

Can an agent recover after being steered or constrained?

Yes. When evaluate reports no trip condition during a beat, tick de-escalates the agent by exactly one level toward healthy. This one-step recovery design prevents state flickering and allows agents to resume normal operation after sustained good behavior.

Where does the breaker receive the usage data it evaluates?

Per-agent counters are populated by external callers such as src/main/hookServer.ts, which invokes recordToolUse and recordError. The heartbeat loop in src/main/index.ts passes fresh BreakerInput objects—containing AgentUsageSample data from src/main/usage.ts—into breaker.tick for evaluation.

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 →