Circuit breakers
The breaker is the hook that bounds an unattended agent: loops, repeated failures, budgets and wall clock.
A tripped breaker ends the run, and nothing works around it. A breaker fires when a session has shown it is not making progress, and continuing would cost money and produce confident, wrong work.
Where it appliesβ
The breaker applies to supervised runs only. A trip commits so the run's work survives the run, which is the wrong thing to do to a person at a keyboard.
The breaker arms when ORION_WORKSPACE is set, which the supervisor exports
into every agent run and nothing else does. Outside one it says so and allows
the call:
orion: not a supervised run (ORION_WORKSPACE unset); breaker inactive
ORION_BREAKER_FORCE=1 arms it anyway, for testing from a shell.
This scoping is the breaker's alone. gate (dangerous shell commands) and
shield (an agent editing its own guardrails, or weakening the test that
defines a fix) guard against anyone holding the tool, and stay armed
everywhere, including your own Claude Code sessions in an adopted repository.
The tripsβ
| Trip | Fires when | Setting | Self-service recovery |
|---|---|---|---|
breaker/unverified-edits | edits pile up with no passing test or build | max_edits_without_verify (25) | Yes. Run the tests or the build. A passing verify clears the trip and the run continues. |
breaker/loop | the identical call, with the same arguments, repeats | max_repeat_identical (4) | None |
breaker/command-failures | one command keeps failing | max_same_command_failures (3) | None |
breaker/consecutive-failures | calls fail several times in a row | max_consecutive_failures (3) | None |
breaker/no-progress | the agent only polls, with nothing else happening | max_consecutive_polls (12) | None |
breaker/tool-budget | the session reaches its tool-call budget | max_tool_calls (400) | None |
breaker/blast-radius | one session edits too many distinct files | max_files_touched (60) | None |
breaker/session-time | the session runs past its wall clock | max_session_minutes (90) | None |
All the settings are under limits in orion.json. At 80% of the tool
budget the breaker warns the agent to start converging, so it can wind down
before it is cut off.
The block message names the recovery this specific trip has. If it offers none, none exists.
What stays open after a tripβ
Two things stay open on every trip:
- Writing
plans/BLOCKED.md, the breaker's own protocol. - The cleanup allowance.
The cleanup allowanceβ
Without it, an agent that trips mid-edit, with Edit and Bash both refused, could not revert the risky change it just made, and the next reader would find a modified file and no explanation. So a tripped session may still run six commands, and only these:
git status git diff git checkout -- <path> git restore <path>
git add <path> git commit
git commit --amend is refused, because it replaces the tip commit, and
what the run already committed is the only durable record a tripped run
leaves.
General Bash is not allowed for cleanup, since a cleanup edit and another
attempt at the task differ only in intent. The listed commands cannot do
anything else: git checkout -- x can only revert, and git commit cannot
change a file's contents. Spending the allowance never clears the trip, and
it does not refill; when it is gone, everything is refused. Compound
commands such as git status; anything are refused too.
Waiting on a long commandβ
The identical-repeat breaker does not count a read of a file you are waiting on. A call counts as a wait when it is:
- asking a background task for its output;
- reading a file a background command of yours was told to write, even while it is still empty;
- a read that returns something different from the last identical one.
Everything else counts, including re-reading a file nothing is writing.
An agent that does nothing but poll trips breaker/no-progress. In a
headless run a backgrounded command is never
announced back, so prefer the foreground. One Bash call that waits
several minutes is one tool call, and waiting is free.
The wip: snapshotβ
When a trip with no way out fires, the worktree's modified and new files
are committed on the spot as a wip: snapshot, before the
agent gets another turn. plans/BLOCKED.md is excluded, since it is the
account of the trip.
The commit message says the work is unverified, and it is preserved for a person to read. Nothing in the cleanup allowance is spent on it, and the block message says what happened to it, including when the commit failed.
breaker/unverified-edits does not snapshot. It is the one trip with a
way out, and a wip: commit in the middle of a run that then succeeds would
be noise in a pull request.
The stop noteβ
plans/BLOCKED.md is appended by the breaker, at the moment it trips,
so the note exists even if the agent never gets another turn.
What an agent should do when it tripsβ
- Stop. Do not retry the call. Do not find another route to the same action.
- Add to
plans/BLOCKED.md: what was being attempted, what is done, what remains, and the exact next step. - Revert what should not survive, then commit what compiles. A branch with real commits can be resumed. A plan file describing uncommitted work cannot, and an uncommitted change also blocks the next rebase of the branch.
- Summarise and stop.
What Orion does when the agent could notβ
A run killed on its turn ceiling, or one whose last act was the call that
tripped, has no turn left to spend the allowance. When a run ends with a
breaker tripped and the worktree still holding uncommitted changes, Orion
commits them as the same wip: snapshot, and says so in the run output,
on the ticket, and in the project's channel. Only if that commit fails
does it revert, and it says that too, because a dirty worktree makes the
next rebase of the branch refuse.
What a person should doβ
orion reset --session <id> # clears the trip, after you have reviewed it
Then requeue the ticket, or let the watch retry it; both are in the recovery runbook.
If a limit is wrong for a repository, change the limit in orion.json
(limits.max_repeat_identical and the rest) and leave the trip alone. See
Tuning the guardrails.
Why loop trips have no way outβ
For an unverified-edits trip, running the check is both the remedy and the proof. A loop trip means the agent is repeating itself, and letting it run a build tells it nothing new. The cleanup allowance makes no attempt at the task: it lets the agent hand over the worktree, and the run still ends.
Breakers above the sessionβ
Two more breakers work at the level of the queue rather than one session:
- The planner evicts a ticket that trips the breaker
limits.max_breaker_tripstimes, or whose worktree fails to settlelimits.max_strandedtimes. Evicted twice, it is held for a person. - A watch that achieves nothing for
limits.no_progress_minutesstops itself and says what it was waiting on.
Both are on the Recovery runbook.