Agent Auto-Restart¶
SASE has a recovery service for agents interrupted by a live update. It classifies the
failure, records its evidence, and can relaunch an eligible agent under the same name.
It is enabled by default and runs through the scheduler's agent_auto_restart job.
An automatic restart uses the same stop, wipe, and relaunch machinery as
sase agent restart NAME, with preserved failure
evidence and the live autonomy profile. Each lineage (the original run and its retry
chain) gets at most one automatic restart. A claimed lineage that is later declined
cannot be automatically retried. Manual restart remains available.
When a restart happens¶
The classifier recognizes specific update-skew signatures and checks corroborating evidence. W1 means the runner's boot code identity differs from the current tree; W2 means an update journal entry falls between boot and failure; W3 is file-level evidence of a changed symbol or culprit commit; W4 is a successful fresh-interpreter import probe.
| Failure family | Core classifier requirement for a relaunch verdict |
|---|---|
| Torn Python imports or module attributes in SASE-managed code | Matching signature, W1 or W2, and W4 |
Stale sase_core_rs binding or wire schema |
Matching signature and W4; without a probe, W1 or W2 permits deferral for probing |
| Data-format skew, such as "written by a newer sase version" | Matching signature and W1 or W2 |
Managed-code TypeError signature mismatch or NameError |
Annotate only; never automatically relaunch |
W3 helps explain and group the incident; it does not independently authorize a relaunch. The healer also runs an import probe before executing a relaunch, including for a data-format verdict. A successful import checks that current modules load; it does not run the agent's task or its tests.
The healer also waits for quiescence: no update holds the code-swap writer lock and
the tree has been quiet for quiescence_seconds. A pass that cannot act yet defers.
Deferral expiry is measured from the lineage's first claim (the earliest ledger
history[].at, else first_claimed_at, else claimed_at): once max_defer_seconds
have elapsed since the first claim, the healer declines the lineage and does not loop
forever. A silenced row escalates with "Couldn't restart \
For a Python or binding failure whose first verdict is defer (probe_pending), the
healer runs the import probe and classifies again with that W4 witness. It relaunches
only when the second verdict is relaunch.
Who the healer considers¶
A failed row is a healer candidate only when at least one of these holds:
- It has a doorbell.
- Its
done.jsonrecovery.stateis in flight (pending,deferred, orlaunching). - Its
failure_facts.skew_suspectis true. - It is a legacy row: no
failure_factsand norecovery, it died withinmax_defer_secondsof the tick, and its error, traceback, or runner-log tail matches the Tier 1–3 prefilter.
Dismissed bundles are never healer candidates; scan may still read them. Rows that are
not candidates get no ledger record, no done.json write, and no notification.
write_recovery and every other done.json writer in this feature write only when
done.json already exists — they never recreate a wiped or dismissed artifacts
directory.
sase agent auto-restart run -p and the scheduler job share this candidate rule.
--dry-run (-n) is read-only: it claims nothing, writes no ledger or done.json, and
does not notify.
Phases and modes¶
Current runners write lifecycle markers. A known death before the model turn
(booting, waiting, preparing) is eligible; a known later phase is not:
| Death phase | Mode | What happens |
|---|---|---|
| Before the model turn | relaunch |
Relaunched once under the same name |
After the model turn (provider_running and later) |
notify_post_provider |
Not relaunched; you are notified with held-workspace guidance |
| Plan, question, monitor, gate, or pipe handoff | ask |
Not relaunched; you are asked to decide |
Older records without lifecycle markers use traceback and log heuristics. An unknown
phase never relaunches: frames without run_execution_loop, invoke_agent, or
finalizer frames, or a refresh log line with no provider start, count as pre_provider;
otherwise the classifier asks with reason phase_unknown.
Never restarted: provider errors, rate limits, auth, kills and cancels,
OOM/timeout/disk-full, directive or macro errors, tool and test failures, and any
ImportError from workspace or third-party code. Remote rows are also excluded.
Anything the restart planner refuses is also left alone.
Skipped on purpose, every time: killed or dismissed agents, agents mid-,x, and agents
holding a question or gate. User intent wins.
The ledger and the one-restart guarantee¶
Claims live outside every artifacts directory under ~/.sase/agent_auto_restart/ so a
workspace wipe can never remove them:
ledger/<project>__<lineage_root>.json— one record per lineage, claimed atomically before any mutation.doorbell/<project>__<artifacts_timestamp>.json— dropped by the dying runner; deleted once a ledger record owns the failure.episodes/<episode_slug>.report.json— the live per-episode report, re-rendered on every change.state.json— storm-breaker pause state.
The state machine is claimed → deferred | declined | launching, then
launching → launched | settled_failed and launched → settled_ok | settled_failed. A
replacement that fails again is reported and never retried. When a claimer died
mid-launch, a later pass adopts the replacement it finds or settles the record as failed
(launch_aborted) — it never launches twice, because forced name reuse would wipe the
replacement it just started. Ledger-only orphan records whose failed done.json is gone
resolve without touching the row: stale claimed/deferred records decline quietly as
row_gone (no done.json write, no notification, doorbell deleted), while stale
launching records go through the adopt-or-settle path.
Update episodes are grouped by culprit commit (for example sase@9fd8a08), so one
update that breaks five agents produces one story, not five.
CLI¶
A bare sase agent auto-restart delegates to list, which shows pending claims, newest
first and grouped by update episode. Use list -a to include launched, settled, and
declined records.
| Command | Purpose |
|---|---|
list [-a/--all] [-j/--json] |
Pending ledger claims (claimed, deferred, launching); -a includes launched, settled, and declined records |
resume |
Re-arm after the storm breaker trips |
run (NAME \| -a/--artifacts-dir DIR \| -p/--pending) [-n/--dry-run] [-j/--json] |
Run the healer; -p is the scheduler job's target and a manual run still honors the ledger |
scan [-j/--json] [-l/--limit N] [-s/--since DURATION] |
Read-only replay of the classifier over history; never writes the ledger |
show TARGET [-j/--json] |
One card: verdict, witness checklist, state timeline, evidence paths |
sase agent auto-restart list -a
sase agent auto-restart show my-agent
sase agent auto-restart scan -s 7d -l 50
show accepts a ledger key, lineage root, or agent name. scan defaults to the last
seven days and 50 displayed rows; -l must be positive and limits only the human table.
The summary and -j output cover the entire matching set. A scan collects evidence and
classifies history without running W4, so defer can mean that the import probe is
still needed.
For a non-empty resolved target set, run -j returns one JSON envelope and currently
exits 0 even when an individual outcome is error. Inspect every outcomes[].action
and reason. Without -j, an error outcome exits 1. An empty target set prints
No pending failures to heal. even with -j; a disabled healer exits 3 without a
JSON envelope.
Configuration¶
agent_auto_restart:
enabled: true # permanent kill switch
quiescence_seconds: 30 # tree-quiet wait before acting
max_defer_seconds: 1800 # longest a claim waits for quiescence
pending_resurface_seconds: 600 # stale pending rows are re-surfaced loudly
storm_max_per_episode: 12 # storm breaker: launches per episode
storm_max_per_30m: 20 # storm breaker: launches per 30 minutes
Set enabled: false to disable automatic recovery. run then refuses before claiming
or relaunching anything. While disabled or paused, the scheduler resurfaces pending
failures instead of submitting healer work, so that scheduler path does not spend a
lineage's restart. A manual run during a storm pause can still claim or decline a
record; clear the pause with resume before requesting recovery.
See the configuration reference for
field types and defaults. resume clears a storm pause; it does not turn a disabled
setting back on or reset a lineage's record.
Notifications¶
One upserted amber ↻ row per update episode (sender=agent.auto-restart,
action=ViewReport). The first relaunch creates the row and its single information
toast; every further relaunch in the episode appends one +1 note without toasting and
refreshes the title and the inline report snapshot in place. The title never contains
"fail" or "error"; exception text appears only in note 2 or later.
The live report (headline, update/culprit/witness facts, per-agent rows with a live Now column, left-alone bullets, why-text, prevention line) is linked from the row. A settlement tick refreshes both the report file and the episode row's inline snapshot without moving delivery cursors, so Now settles without a re-toast.
A row was silenced when its runner dropped a doorbell or wrote
recovery.state = "pending" (the failure notification went out silent=True). Every
terminal outcome of a silenced row is made visible exactly once, with the escalation
copy below (escalation where the plan specifies one, otherwise resurface_failure). A
row that was not silenced already notified the user: a skew-shaped decline records the
ledger and shows only the dim Agents-tab hint (quiet_declines = quiet). It does not
get a second resurface_failure or escalation. Two exceptions stay loud regardless:
already_restarted for a replacement's second failure, and the single storm escalation.
Escalations are loud and land in the Errors bucket: post-provider deaths name the held
workspace, declined relaunches name the reason, re-broken replacements say sase will not
retry, and a tripped storm breaker pauses the feature until
sase agent auto-restart resume. The storm row is sender=agent.auto-restart at error
severity so it routes to the Errors tab. The pass that trips sends one storm escalation
for the episode; later targets while paused decline quietly as paused.
In the Agents tab, in-flight recoveries render as amber ↻ RESTARTING with a dim reason
instead of red FAILED. The replacement keeps the same name with a ↻ chip and a
provenance block naming the update, the signature, and the preserved evidence files
(error_report.md plus the bundle's individual files, opened with the existing v key;
no directory is registered).
Dependency waits remain parked while recovery is pending, deferred, or launching.
Name-based waits find the replacement by name; waits pinned to the old artifacts
directory can follow the ledger's replacement mapping after the wipe. They then wait for
the replacement's actual outcome.
Troubleshooting¶
auto-restart never ran — is the sase scheduler running?The row satpendingpastpending_resurface_secondswith no ledger record. Start the scheduler; nothing was claimed or spent.- "Couldn't restart \
automatically — \<reason>." The classifier, probe, or quiescence gate declined. Press,xon the row to retry by hand. - "This was its automatic restart — not retrying." The replacement broke again. Its one restart is spent; investigate it as a real failure.
- "Auto-restart paused: N agents broke within one update — this looks like a real bug,
not an update race." The storm breaker tripped (N counts the already-launched
records plus the current target). If the update really is that breaking, run
sase agent auto-restart resumeto re-arm. sase agent auto-restart scan -s 120dreplays the classifier over history read-only. Zerorelaunchverdicts among non-skew failures means the classifier is not over-eager.- Evidence. Every relaunch preserves the error report, traceback, log tail, facts,
and verdict in the recovery bundle linked from the ledger record (
show TARGET) and the episode notification.
sase agent auto-restart¶
The scheduler's recovery service classifies update-skew failures and can relaunch an
eligible agent once per lineage. Use list for pending claims, list -a for the full
ledger, show TARGET for one record's evidence, and scan for read-only historical
classification. run NAME, run -a DIR, or run -p invokes the healer; resume
clears a storm pause. The configuration switch is
agent_auto_restart.enabled.
A bare invocation delegates to list. run --dry-run is read-only, and a successful
run -j exit does not guarantee a relaunch. See Agent Auto-Restart for the
options, output contracts, and current probe/deferral limitation.
_[project]: A sase project is a named unit of work registered with SASE. _[workspace]: A sase workspace is a numbered clone of a project's primary repo, managed by the workspace store and tracked in that project's registry.json. _[agent]: A sase agent is an agent session or a single agent that does not belong to a session. _[prompt]: A prompt is the text SASE hands to a provider to start an agent turn. _[macro]: A macro is a reusable prompt triggered with #name in agent prompts. _[Patch]: A Patch is SASE's local unit of change. _[bead]: A bead is one tracked work item in SASE's git-native issue tracker. _[plan]: A plan is a persisted, approved artifact recording the intent behind agent work. _[TUI]: The TUI is SASE's terminal interface for running agents and reviewing results. _[epic]: An epic is an executable multi-phase plan run with sase bead work. _[gate]: A sase gate is a durable, command-backed request for one user decision, created when work must pause for the user. _[monitor]: A sase monitor is a monitor turn that runs one long command under a detached supervisor, so the command outlives the agent that started it. *[stitch]: A stitch is the lightweight ordered change record inside a Patch's STITCHES: section.