Yesterday a cheap model spent an hour failing to diagnose an SSH outage. It rejected the correct cause when I gave it to it, and four minutes after I switched to a stronger model by hand, the problem was solved.

Nothing in the session could see that the agent was stuck. A budget rule had put the cheap model there, and no rule knew how to take it away.

This week I wrote three small extensions for the oh-my-pi coding agent. Each one changed one setting of a running session:

Extension Setting Post
decaying-effort Thinking effort, high at the start, lower later Decaying Effort
model-shift Which model, chosen by CEL rules over cost and turns Policy-as-Code
model-switch-prune What context is sent, by trimming old tool output Lighter Sessions

Each one worked on its own. Together they fought. This post is about the merge, and about the fourth setting the merge made easy to add: escalation when the agent says it is stuck.

Why three controllers is the wrong number

Three controllers that act on the same session at different times cause three problems:

  1. Each one invalidates the prompt cache on its own. A model switch makes the cache cold. A prune changes the prompt prefix, so the cache is cold again. When the two happen on different turns, you pay for two cold reads instead of one.
  2. Each one has to tell the others’ changes from yours. The effort controller needed special code so that a model switch did not look like a manual effort change. The model controller treated a /model as “the human took over” and stopped all of its rules, including the effort part of each rule.
  3. No one owns the policy. Three config files, three sets of rules, and no single place that answers “why is this session on this model at this effort right now?”

Control engineers know this pattern. Put several independent loops on one plant and they interact: one loop’s output is another loop’s disturbance. The usual fix is one supervisor that sees every sensor and owns every actuator.

The governor

session-governor-omp is that supervisor. It has one rules file, one evaluation point, and one log.

rules:                     # ordered; first match wins
  - name: away
    when: 'afk'
    use: '@smol'
    revert: true           # restore model and effort when I am back

  - name: stuck-escalate
    when: '(blocked_streak >= 3 || attempts_on_goal >= 6) && !model.startsWith("big/")'
    use: '@slow'
    effort: high

  - name: spend-cap
    when: 'cost >= 1.0 && model.startsWith("big/") && blocked_streak == 0'
    use: '@mid'

  - name: context-epoch
    when: 'tokens > 80000 && turns_since_prune >= 4'
    prune: true
    repeat: true

  - name: effort-high
    when: 'turns >= 2 && cost < 1.0'
    effort: high
  - name: effort-auto
    when: 'turns >= 4'
    effort: auto

The structure is the same as in the policy-as-code post, but the scope is the whole session:

Part What it is
Sensors CEL variables: cost, tokens, context_pct, turns, elapsed_min, model, agent, afk, turns_since_prune, blocked_streak, attempts_on_goal
Policy An ordered list of CEL rules. CEL is not Turing-complete, has no side effects, and is type-checked when the config loads
Decision decide(rules, vars, state) is a pure function, so unit tests need no agent
Actuators use: (model), effort:, prune:; revert: and repeat: change how a rule re-arms
Evaluation point agent_end, between two prompts, the same moment a human would type /model

Because there is one decision per boundary, a rule that switches the model and prunes in the same step costs one cold cache read.

Who wins: the human

A governor that fights the human is worse than none. The rule is simple: the human wins, but only on the setting the human touched.

You change Paused Still active
The model (/model) after a governor switch Rules with use: Effort and prune rules
The effort after a governor effort change Effort-only rules Model and prune rules

Both pauses are written into the session as custom entries, so they survive a resume. The governor tells its own changes from yours by comparing with what it last applied. For effort, it compares the selector the user picked, not the resolved level, because auto resolves again on every prompt.

The fourth sensor: let the agent say it is stuck

The missing sensor was the one I needed yesterday. Cost and turns are easy to measure. “This agent is going nowhere” is not.

I looked at three ways to sense it:

Approach Problem
Watch the user (repeated corrections, frustration words) Slow, and it needs the human to be annoyed first
A second model as a judge Costs money every turn, and is one more model to trust
The agent reports its own progress Cheap. Is it honest?

I chose self-report. The governor registers a small tool, progress, that is always loaded:

{ "goal": "find why ssh to the lab host fails",
  "status": "blocked",
  "evidence": "Permission denied (publickey) again after key reload" }

The agent calls it once per turn. The governor reads those calls back from the session branch and computes two variables:

The second counter matters more. Yesterday’s model never said “blocked”. It said “progress” again and again while it made none. Six reports on one goal without a done is a signal, whatever the status says. A new goal text resets both counters, so normal chat does not count against the agent.

Because the counters are derived from the branch and not kept in memory, they are correct after a resume and they follow you when you move to another branch.

A live check ran a real agent in RPC mode. Two different models called the tool on their own, without any prompt asking them to:

"progressCalls": [
  {"goal": "Reply with the single word: one", "status": "done", "evidence": "one"},
  {"goal": "Reply with the single word: two", "status": "done", "evidence": "two"}
],
"passed": true

What is not proven yet

I am not the first to route an agent between models. pi-model-router chooses a high, medium or low tier for each turn from task intent, budget, context size and keyword rules, and sets the thinking level for each tier. It is a good answer to the question “which model for this prompt?”.

The governor asks a different question: “what should this session look like now?”. The differences:

I did not find another tool that combines these four. That is a weak claim, and I would like to hear about prior art.

Source