A failed run keeps its bindings
A top-level run installs each binding as it lands. When the run then fails,
nothing is undone: a let that completed before the failing statement stays
bound in the session, and so do the effects everything before it caused. There
is no rollback, and none is wanted.
The law is normative in docs/SPEC.md §5.6, with its worked example: after a
failed run of let before = 1 / failing-command / let after = 2, before
remains bound and after does not exist.
The mechanism. run_phrases (core/src/evaluator.rs) runs each top-level
phrase in order in one loop. Under Mode::Session, Ran::define writes
the landed binding through shell.note_define and back into shell.env
before the next phrase runs; a failure breaks the loop and touches neither.
Ran::env is therefore env as extended by the phrases that ran before the
halt, never rolled back.
Every ending, not just the tidy ones. A raise and a wall are two
classifications of the same Err(Break::Error(_)) return: both leave dispatch
inside RunReport::Ran and so return through Shell::enter’s Ok arm, which
never touches the checkpoint. mid_script_wall_keeps_the_bindings_that_landed
(core/src/run.rs) pins this for the wall specifically, with the cancel landing
mid-script rather than before evaluation. Exarch pins the raise from its own
side in tool_call_partial_effects_persist_on_error
(exarch/src/shell_eval.rs), and core/tests/top_level_vs_block.rs is the
contract’s home.
The one rollback in the tree is not an exception to this. Shell::enter
checkpoints (env, context) at entry and restores it in exactly one arm — the
Err(payload) arm of catch_unwind, which reports run panicked: …
(panicking_run_reports_failed_and_rolls_back). That is host-panic recovery,
reachable from no ral-level failure. A raise, a wall, an exit, a non-zero
command exit: none of them reach it.
Transactionality is rejected, permanently. A ral script’s statements cause
real, external, irreversible effects — it spawns subprocesses, redirects into
files, and detaches workers that outlive the run — and there is no journal of
any of that anywhere in core/src. A transaction that reverted the lets
while the file the failed script wrote stayed on disk would be worse than the
honest semantics: the name vanishes and its effect persists, so the model can no
longer name what it caused. Partial rollback of a partially-reversible run is
not a weaker guarantee than none; it is a misleading one. It would also
introduce a rollback concept into the language, a notion ral has deliberately
never had, to serve a case that concept cannot cover.
Why the prose matters as much as the mechanism. Believing its bindings were
discarded, an agent recovering from a mid-script failure replays the script from
the top — and the earlier statements’ effects were real, so replaying re-runs
them. A false claim of rollback therefore converts a recoverable failure into
duplicated side effects. This is why exarch’s failure text
(exarch/src/shell_eval/report.rs), its system prompt (exarch/data/ral.md
§Failure), and shell-eval all state the truth
explicitly rather than leaving it to be inferred.
This is a hard rule. Do not make a submitted program transactional, do not widen the panic checkpoint to cover a ral-level failure, and do not write model-facing text that says a failed call’s bindings are gone.