A run, end to end
A run is one top-level evaluation against a persistent Shell, and every
host starts it through a synchronous, runtime-agnostic run door —
Shell::run(RunRequest) -> RunReport (or Shell::run_under when the host
already holds a foreground scope) in
core/src/run.rs. The request’s Run
carries a Program sum naming what runs: source text, or a registered hook
applied to first-order arguments — a Block/Lambda the host stored by name
in the session-lived hook table (Shell::register_hook), so the host conveys
data, never closures, across the dispatch boundary. No host reimplements
evaluation; each is a request supplier that hands the door a RunRequest and
renders RunReport its own way
(unify-turn-evaluation,
run-turn-host-loop). Completion is the
door returning — never a channel disconnecting — so a detached spawned
worker (a server, a watch) holding a clone of the surface cannot keep a run
from ending (run-turn-host-loop). The
reduction primitive behind the door is crate-private, so a host cannot start an
unframed evaluation against a stale frame
(run-turn-is-host-api).
There are three doors, and which one you use is decided by what you hold.
Shell::run is for a host with no run in hand: its frame is minted under
SessionState::anchor. A host that already holds a pre-minted
ForegroundScope uses Shell::run_under(&scope, req); the identity and wire
transports use this form so cancellation can land before a frame exists. Code
already inside a run — a builtin body, a lifecycle hook — uses
Shell::run_nested(&mooring, req), handing it the Mooring it was given, so
the nested frame is a child of the enclosing run’s cancel scope: the outer
run’s interrupt unwinds the nest, and the outer wall reaches into it
(cancel-is-a-watermark).
The request carries the policy axes as types, never flags. A RunRequest
is exactly the places hosts differ:
- IO — a
RunIoregime sum over byte output.Inheritruns on the session’s ambient streams (cloned, then restored);Capturehas core mint fresh stdout/stderr buffers it returns inRunReport::Ran’scaptured. - Stdin — a
RunStdinchoice, orthogonal to the output regime:Inheritreads the session’s fd 0 (terminal, pipe, or file),Emptyinstalls an immediate-EOF source with no fall-through. - Terminal — a
RequestedTerminalAccess:Leasedmay foreground a terminal-bound child,Deniedmay not.Captureno longer implies terminal ownership — a pipedral -cisDeniedyetInherits its stdin (terminal-lease). - Capabilities — the
Capabilitiesceiling pushed for the run’s dynamic extent (root()for the REPL, a grant profile for exarch). - Limits —
wall(the foreground deadline) anddeferred_lease/worker_cap(the idle/backstop lease and the concurrency cap governing workers the run defers at the durable root). - Surface — an optional run-local
SurfaceSink(Arc<dyn EventSink>), installed only for this run;Noneis the identity. - Lifecycle — optional pre/post-exec hooks (
Box::new(())for a host with none). - Trail —
trail: Option<CapturePolicy>(audit).Somedelimits this dispatch’s own extent as an audit scope and carries it home onRunReport::Ran;Noneneither opens a scope nor collects one.
The spine the run doors orchestrate is one straight line, owning resources
(Sink, Source, the run’s frame, guards, buffers) while the request describes
policy. The run doors dispatch on the Program sum, and the arms differ only
in how the program resolves — the source arm compiles first, the hook arm
looks up the hook table; both then converge on run_built and the run
module’s framed scaffold:
- Mint the run’s foreground scope — a child of the scope the door was entered
under, so a foreground timeout never reaches a detached worker
(cancellation) — and arm its wall, if any,
before compiling, so
wallbounds the whole run — compile and typecheck included, not only evaluation.compile_run’sprocess::cleartouches only the signal count, never the reaper, so the armed ceiling survives the compile. - The source arm’s
compile_runrunscompile_and_typecheckseeded from the live session’s schemes (the ladder; session-scheme-continuity). A parse or type failure returnsRunReport::Static { diagnostics }at once — no run state, no root context, no hooks; the host renders the diagnostics and treats it as status 1. The hook arm skips this: its program is an already-compiled value resolved by name in the hook table, and the hook’s registeredDefaultPolicy(capture, terminal authority) folds into the run’s conditions — the hook’s to decide, not the dispatching host’s. run_builtmaterialises the IO regime —Capturemints the buffers it reads back,Inheritleaves the ambient streams to flow — then assembles the run’s frame in two halves, split by mutability. What the run fixes once (thesurfacesink, the deferred rail with itsdeferred_leaseandworker_cap, the desk, the nursery, the foreground scope, and the run’s terminal authority) is aMooring, an owned local onrun_built’s own Rust stack frame; the surface has no liveness role, so a clone of it can never define run completion. What genuinely changes within the run is taken on loan: the byte streams — theIobuild_runseeds from the ambient session — and the twoCopyregisters the run’s frame owns for its life, the root-source registersession.root_fileand the dispatch registerlocal.audit.call_site.run_framedborrows the mooring, installs the run’sIothrough anIoLoan, and evaluates. The loan is RAII over that one swap: it moves the new streams ontoshell.ioand restores the previous ones onDrop, so teardown survives a caught worker panic. The mooring needs no guard — it never moved, so an outer run’s is back the instant this stack frame ends, and theNurseryGuardbeside it empties the nursery on the unwinding path as surely as on the clean one. The root context is installed, the pre-exec hook fires (taking&Mooringbeside the shell, as every in-run body does), andwith_capabilities(caps, body)runs the run’s program under the request’s capability ceiling —run_phrases(&top.phrases, shell.env.clone(), Mode::Session, mooring, shell)for the source arm, the in-framebuiltins::applyof the resolved hook for the hook arm (the machine; unify-turn-evaluation).run_phraseswrites eachletstraight intoshell.envas itsDefinelands, and acd,alias, or hook registration is an unbracketed write straight toshell.context— there is no post-run install step, so a halted run keeps every write that landed before the halt and the run remains a resume point regardless of completion, error, orexit(exchange-ends-ready). Before the status is read,run_framedpollsprocess::check(mooring)once more so a sticky cancellation cannot be absorbed bytry; it then computes the transport status, fires the post-exec hook, and emits ready-boundary notices while the run frame and sinks are still installed. Only then does the IO guard drop.- Back in
run_built, the wall is disarmed before the cause is read, so a reaper tripping in the gap between eval returning and classification cannot misread a run that finished inside its budget as timed out.classify_endingthen folds the settledResult, the transport status,single_command,root, and whether aDeadlinecause genuinely elapsed into onerun::Ending—Settled,Raised,Walled,Exited, or (unix)Stopped— soOkbeside a stray “timed out” flag is no longer a state the type can hold.RunReport::Ran { ending, captured, trail }carries it home;trailstartsVec::new()here —run_builthas no view of the dispatch’s own scope, only of what a body opened and closed on its own account. - One level up, at
Shell::enter— the durability wrapper all three run doors funnel through — aRun.trail: Someholds aTrailScopeoutside thecatch_unwindthat recovers a mid-run panic, opened beforedispatchruns and closed once it returns, on every exit. That placement is load-bearing: the(env, context)checkpoint the panic arm rolls back does not coverlocal.audit, so only a scope the panic itself cannot skip keeps the trail’s close law true at dispatch granularity. A clean exit’s observations land inRunReport::Ran.trail; a caught panic’s are drained and discarded — the panicked dispatch reportsStatic, never a trail.RunReport::into_reportthen renders the engine’sEndingagainst theSourceDb— aRaised/Wallederror becomes the string the host prints verbatim,command_exit/statuscomputed alongside it — onto the wire’s ownprotocol::Ending, and projects eachObservationthrough `to_wire` ontoReport::Ran.trail: Vec<FOValue>, unbounded by declaration — the wire’s frame fuse is the shared backstop, as it already is forcaptured.
The hosts differ only in the request they supply.
- The REPL’s
execute_input(ral/src/repl/exec.rs) suppliesscript_name: "<stdin>",Capabilities::root(), no limits,RunIo::Inherit,RequestedTerminalAccess::Leased,RunStdin::Inherit, no surface, and thepre-exec/chpwd/post-execplugin hooks; it builds aProgram::SourceRunand drains it throughprotocol::dispatch_to_reporton its prompt thread with the muteArc::new(())host, rendering the terminalReportwithprint_result— anErr(Severed)prints the cause and ends the line instead. Its plugin hooks and prompt body (ral/src/repl/plugin.rs,prompt.rs) dispatchProgram::Hookruns instead — hooks the REPL registered by name, run through the same frame. - exarch’s
run_shell(exarch/src/shell_eval.rs) suppliesscript_name: "<tool>", its session grant profile, a per-toolwalland a 1 h idledeferred_leaseunder a 24 h backstop, plus aworker_capon concurrently running workers,RunIo::Capture,RequestedTerminalAccess::Denied,RunStdin::Empty,trail: Some(CapturePolicy::Off); it builds aProgram::SourceRunand drains it throughprotocol::dispatch_to_reportagainst its call’sRunHost— oneHostpairing the enquiry desk with the applier that renders each live surface value onto the presentation bus — and renders the cappedToolResult, or folds anErr(Severed)intoOutcome::Severedand the one sentenceagent::seat::engine_gonerenders for it (agent). The pushed grant frame is the sandbox — ral’s grant, not a source-levelgrant { … }the model could escape — which is why exarch needs no runtime of its own (exarch-architecture). - ral’s batch path (
ral/src/batch.rs) suppliesRunIo::Inherit, no surface, and a()lifecycle, withRequestedTerminalAccesskeyed to whether it owns the terminal — the third source-run client ofShell::run, closing the one entry unify-turn-evaluation flagged.
The human and the model are interchangeable suppliers of top-level runs over
one persistent Shell.
See also compilation-ladder, evaluator-machine, cancellation; run-turn-host-loop, run-turn-is-host-api, unify-turn-evaluation; maps repl, exarch.