The first layer never changes the agent. It only finishes runs or stops them.
The second layer suggests changes to the agent but never makes them.
This page is an overview. Each section links to the page with the full details.
Recover a run now
Give the agent a checkpoint store and startagent.recover() once per process.
Every intervalMs (default 30 seconds) it lists agent.pending() and resumes
each interrupted run from its last checkpoint, so completed steps don’t run
again.
recovery.stop(), or abort the signal option, on shutdown.
What the loop does with failures:
- Backoff. A run that failed is skipped until its backoff passes:
intervalMsdoubled for each failed attempt in a row, up to 15 minutes. - Quarantine. After
maxAttemptsfailures in a row (default 10), the loop stops resuming the run and callsonQuarantined(run)once.agent.pending()still lists it, andagent.resume()still runs it, so a person can decide. Saving progress resets the count. - Retry with a diagnosis. With
retryWithDiagnosis: true(defaultfalse), a run that reaches the cap first gets one more try: a fork named<id>.heal, resumed from the same step, with a note saying why the run failed. The note quotes the error as data and caps its length. If the fork finishes, the original is deleted andonRecoveredreports the original’s id. If the fork also reaches the cap, both are quarantined. This applies tosend()runs, not session turns, and the store must keep a checkpoint history. - Errors that won’t go away. A run whose last error has
retryable: false, such as a 400 from the provider, is left alone. - Provider outages. After 3 provider failures in a row, across any runs, the
loop pauses and calls
onOutage(true). It then tries one run at a time, at growing intervals, and callsonOutage(false)once one gets through. Runs aren’t quarantined for provider failures while it’s paused. A bad API key doesn’t count as an outage. - Approvals. A run paused on an approval or a question is left for a person.
recover(): run leases
make sure only one of them resumes a run. See
Recovering automatically for
the full behavior.
Stop a run that makes no progress
Recovery restarts runs that were cut short. A run that is still going but stuck, such as one calling a failing tool again and again, needs a hook instead.createProgressGuard() adds a hint to a tool result that keeps failing the same
way, then denies the tool or the repeated call. Once it has denied maxDenials
calls (default 3), the next denial stops the run with
LOUSHO_RUN_NO_PROGRESS. That error is
recorded as not retryable, so agent.recover() won’t resume the run.
Each hint, denial and stop is streamed as a heal.action event:
createOutputCheck(). See Hooks
for both, and Stream events for the heal.action fields.
Watch it
- The
recover()callbacks:onRecovered,onQuarantined,onOutageandonError. A throwing callback doesn’t stop the loop. agent.pending()lists every unfinished run withlastErrorandattempts, including quarantined ones.- A result the progress guard replaced is marked on the tool’s span as
lousho.tool.replaced_by_hook, and a denial is recorded as the span’s error. See Tracing and observability for spans and the local observe daemon.
Propose edits for later
improveAgent() from lousho/improve looks for better instructions and skills
using failing cases you give it. It’s experimental.
- It only returns a proposal. An
outcomeofproposalcomes with adiff. A person reviews it, withformatImproveResult()for a readable card, and applies it by hand. - It can only change instructions and skills. It can’t change tools, permissions or limits.
- It tests before it proposes. Each candidate runs your cases again and must
improve at least one training case without making any case worse, including
held-out cases the editors never see. It also checks the cost per passing run
against
maxCostIncrease(default 0.5). - Your
proberuns the agent, so it owns side effects. AddcreateShadowHook({ readOnly: [...] })unless every tool is safe to run.
casesFromFailures(db) turns failed runs in
your local observe database into cases. Tool results in them reach the editors
marked as untrusted.
From the command line, lousho improve <agent> --check <module> does this and prints the proposal for review; see CLI.
See Improving an agent for the gate, the outcomes and the safety
notes.
How the layers fit together
A run the first layer gives up on, quarantined or stopped withLOUSHO_RUN_NO_PROGRESS, is a sign that something in the agent needs to change.
Fix the tool, or change the instructions. If the cause is in the instructions or
skills, those failures can become cases for improveAgent(), and a person
decides whether to apply what it proposes.
Live evidence (S29 and S30)
In current SDK releases, the self-healing loop (lousho improve on an agent
directory) applies an edit that passes its checks by default (auto mode), and
manual mode queues it for a person instead.
On 2026-11-09 a live run tested that loop on synthetic coding fixtures. The
agent ran on qwen/qwen3.7-flash. A separate editor model,
stealth/crouton-alpha, ran on the synthetic fixtures only. Each scenario ran
once, and planted-editor ran three times.
- Every scenario that tested something passed.
planted-editorpassed 2 of 2, and one more attempt never reached the editors, so it wasn’t counted. - The audit log verified in every attempt directory.
- The report’s verdict is INCOMPLETE: the go/no-go bars need 5 to 10 attempts per scenario.