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, or builtin registered 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, and no front-end calls the door itself: each is a
run supplier that dispatches a Run through a transport, whose engine
(Engine::run) enters the door and projects the RunReport onto the wire’s
Report for the front-end to render its own way
(unify-turn-evaluation,
run-turn-host-loop). Completion is the
door returning — never a channel disconnecting — so a 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 two 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 dispatch scope (Scopes::open’s view) 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 — opens no second run: it applies a
function under the Mooring it was given, so the outer run’s interrupt and
wall reach whatever it calls
(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.Capturedoes not imply terminal ownership — a pipedral -cisDeniedyetInherits its stdin (terminal-lease). - Capabilities — the
GrantStackceiling pushed for the run’s dynamic extent, every layer of it (GrantStack::root()for the ral hosts, a grant profile for exarch). - Limits —
wall(the foreground deadline),worker_cap(the cap on concurrently running workers),cleanup_grace(how long a halting worker or combinator waits for the children it cancels), andworker_title(whether aspawnmust carry a title). - Surface — an optional run-local
SurfaceSink(Arc<dyn EventSink>), installed only for this run;Noneis the identity. - 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 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. The diagnostic carries theSourceits carets point into rather than registering it: a run that failed to compile leaves no live span, so its text has no business in a registry that is append-only precisely because live spans index it (static-diagnostics-render-at-the-seam). 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, andaside— run on ajoin_sessionsibling, so nothing flows back) folds into the run’s conditions — the hook’s to decide, not the dispatching host’s. A fault in it is labelled byHookName::fault_label(plugin 'p' hook 'h',prompt), and a hook run is no ready boundary: only a source run emits the boundary notices. 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 itsworker_cap,cleanup_graceandworker_title, the desk, theForkarm, 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 — for a source run only, a hook having no text to register — andwith_layers(caps, body)runs the run’s program under the request’s capability ceiling, pushed layer by layer and removed by position after, so a session frame the program pushes survives the run —run_phrases(&top.phrases, shell.env.clone(), Mode::Session, mooring, shell)for the source arm, the in-frameevaluator::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_frameddisarms the wall (waiting on a firing already claimed) and makes the closing poll,final_pollon the run’s node, so a stop that landed after the body’s last poll — a key a terminal loan heard back and struck on the node included — is not lost; a stop is an escape notryabsorbs anyway. It then computes the transport status 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,classify_endingfolds the settledResult, the transport status,single_commandandrootinto onerun::Ending—Settled,Raised,Stopped, orExited— a wall being simply aTimedOutstop, so no separate “timed out” flag exists to disagree with the result.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 reportsRunReport::Faulted, never a trail.RunReport::into_reportthen renders the engine’sEndingagainst the shell — aRaisederror or aStoppedstop becomes the string the host prints verbatim and the{cmd, status, message, site}recordtrywould hand its handler,command_exit/statuscomputed alongside it; aSettledvalue the wire cannot carry (a handle, block, or function) becomesUnreturnable, its own rendered failure with a hint; aStaticrenders the same way throughStaticDiagnostics::render, which also settles its exit status (2 parse, 1 type) — onto the wire’s ownprotocol::Ending, and projects eachObservationthrough `encode` 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
exec::step(ral/src/repl/exec.rs) suppliesorigin: SourceOrigin::named("<stdin>"),GrantStack::root(), no limits,RunIo::Inherit,RequestedTerminalAccess::Leased,RunStdin::Inherit; it builds aProgram::SourceRunand drains it throughprotocol::dispatch_to_reporton itsIdentityTransportwith the REPL’s ownHost(ReplHost), rendering the terminalReportwithprint_result— anErr(Severed)prints the cause and ends the session. Thepre-exec,chpwdandpost-execplugin hooks are dispatches of their own around it, as are the prompt body and every other hook (ral/src/repl/plugin.rs,prompt.rs):Program::Hookruns of hooks registered by name, through the same door (loop). - exarch’s
run_shell(exarch/src/shell_eval.rs) supplies a virtualoriginnaming the call’s turn (turn N), its session grant profile, a per-toolwall, aworker_capon concurrently running workers, a 2 scleanup_grace, a requiredworker_title,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 hands back theResult<Report, Severed>itself;Avatar::ralrenders the cappedToolResult, or the one sentenceagent::seat::EngineLostrenders for a severance (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) boots anIdentityTransportfrom thebatchinstaller, dispatches the_ral-boothook, then the script as oneProgram::Sourcerun under the mute host, withRunIo::InheritandRequestedTerminalAccesskeyed to whether it owns the terminal — the same door, the same ending law, and the same typecheck as the REPL (startup).
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.