Map: exarch / agent
agent.rs splits the node every fleet member is into two types along who
may touch what (agent-and-avatar).
Agent is the public half, held behind an Arc the fleet shares:
identity (id, name, log_dir), the two cancel layers (token: cancel::Token and the eval-layer reach: InterruptTarget, both in the crate-level cancel.rs), the sender half
of its own mailbox, an owned hot-swappable provider: ProviderHandle, the
per-node config fixed at birth (caps, fuel, role, search, the
resolved system prompt), the single-writer status: Mutex<Status> — { rest, reply, replied_at, awaiting } — and a weak children: Mutex<Vec<Weak<Agent>>>.
role: Role says whom the agent answers: Converses (an attended or
embedded root, rooting its own tree), Headless (the
headless root, replying once to the process’s caller), or Child { parent, consumer } — the strong parent edge and the parent’s Stamp, minted at
birth by Role::child. A parented agent that converses is unrepresentable. Its fields are plain
pub(crate): a record, not an interface, read directly by the modules that
run it. What every node of a run shares — the host’s settings and the
services it opened — is not on the node at all but on the fleet’s
`Launch`, held once. There is one fence, the
inbox’s own clear-epoch
(agent-and-avatar), and it is carried
only inside bus::Stamp — the addressed envelope Mailbox::stamp mints,
pairing the destination mailbox with its epoch at composition — so a
message that cannot judge its own staleness (bus::Stamped) is unsendable
with a stamp forgotten, skewed to another inbox, or refreshed at push time;
the destination judges it at its own pop.
Avatar is the private half: the canonical
model projection (AgentLog, record/session.rs), a seat
(agent/seat.rs) carrying the transport this run drives through —
Seat::Identity, an IdentityTransport booted through the installer’s two
halves (boot::engine_runtime and boot::engine_root, in the table
exarch::INSTALLERS both carriers share) — a root keeps the Attach it was born from and the session Scratch
that Attach names, an adopted fork keeps neither — or Seat::Wire, a
WireTransport driving a remote engine, one process per session, keeping only
the EngineSpec it was attached with
(engine-protocol) — the
canonical run and probe vocabulary either way
(engine-protocol) — plus the inbox, the
reply: ReplyCell (a SharedCell, borrowed rather than locked, as LogCell is for the AgentLog: one thread, so a failed borrow is reentrancy and panics) the desk’s reply handler stages into, the readings
(the token Measure, the Gauges, the Nudges — everything this avatar has
measured and told about one context, reborn whole with it), and the
Arc<Agent> it embodies (.agent,
pub(crate) so a caller outside agent reaches identity and config at
.agent.… directly). Every method that runs the agent takes &mut Avatar.
agent is the last field on purpose: the fleet’s Weak must outlive the
seat’s teardown and the log’s SessionEnded, since a root’s death is what
ends the process.
An agent is live while its Avatar holds that Arc. Nothing
deregisters: Avatar’s Drop (agent/build.rs) clears its own armed
schedules and records the session-ended bookend, and the last strong
reference going with it is what a parent’s children or the fleet’s
roots/names prune away at their next walk. There is no Session, no
is_root: every distinction reduces to position in the tree, read from
the agent’s Role, the launch’s Trunk, and the agent’s own exchange
clock. What every node shares — the Launch, the
by-name index, the idle lease — lives on the `Fleet`, not on
the node; the one `FleetBus` and the transport
Engine are session-level handles the frontend builds and holds, not fields
of Fleet itself. Output caps are fixed agent/digest.rs constants, and the
eviction thresholds agent/gauge.rs’s, not per-agent state.
Status, the exchange clock, and the lock rule
An exchange on this page is a human round — what the fleet’s clock counts and what a cancellation interrupts. It is never a unit of the context, which has only turns, each with a role (an-eviction-is-a-set-of-turns).
Two per-agent mutexes exist, Agent::status and Agent::children. Rule:
hold at most one at a time. children is locked only to push
(Agent::adopt) or to snapshot (Agent::children, which prunes as it
copies); every walk (Agent::walk, descendant, cascade, roster) runs
over the snapshot, never under the lock. status is a single-writer
register — the avatar writes one field at a time (set_resting,
deposit_reply, heard, message, forget) and a reader takes
one snapshot and computes nothing under the lock, so the mutex buys
atomicity and nothing more. parent is a plain Arc, read lock-free.
The park verdict reads no fact another thread can change without a
delivery. Avatar::park_mode runs under the consumer’s own queue mutex
(Inbox::next_or_idle) and before the pop, and each of its inputs is one
of three kinds. The exchange clock is under that same mutex: Queue { posts, last_exchange } is what the mutex guards, so Mailbox::steer and
Agent::message stamp and enqueue in one acquisition (Mailbox::exchange),
and next_or_idle hands the verdict its reading as the engaged argument.
The agent’s own status — reply, and awaiting, the set of direct
children spawned (adopt) or messaged (message) and not yet heard from
(heard, at the moment the attend loop takes up the child’s
AgentResult) — is written only by the consumer’s thread, so
has_busy_children is live children ∩ awaiting and never reads a child’s
own rest. What remains — a child dying, a schedule firing — happens only
after a delivery into this queue, which the pre-pop verdict cannot lose
to. A Status writer drops its guard before pushing to any inbox; that is
the whole of the cross-type lock discipline.
last_exchange lives on the inbox, not on Agent::status. It was never
agent state — it is “when did a human or a parent last push into this
mailbox”, a fact the Mailbox itself witnesses. Agent::engaged() reads
it, and Agent::idle() reads
mailbox.last_exchange().unwrap_or(started).elapsed() for the TUI’s idle age.
The roster’s idle-s reads Status::rest; the reply lease reads
Status::replied_at. These clocks have different origins.
Up is strong, down is weak. Role::Child’s parent is a strong Arc, so
deposit_reply and the scope climb (Agent::descendant) never dangle: a
parent whose own avatar has gone is still a reachable Agent with a
terminated token, not an absence. Agent::children holds Weak, so there
is no cycle to reason about and a walk prunes what has settled. Nothing
holds an Arc<Agent> to a descendant — every reaper closure
(agent/fleet.rs’s lease_fire) and every upward result (AgentResult) carries
a name or a Weak, never a strong handle down the tree.
A root is a parent-less node (parent = None), and a fleet holds any
number: each is a session (design: agents). A run starts in
one of two ways. Avatar::root(RootConfig, RootSeat, provider) founds the
fleet and hatches main (TRUNK_NAME) over a fresh AgentLog::root;
Avatar::resume(RootConfig, RootSeat, provider) lists the run’s live roots
(AgentLog::live_roots: every sessions/<n>/record.jsonl that opens with a
root’s SessionStarted { parent: None, .. } and holds no SessionClosed, in
id order, with its recorded name; a record that cannot be read is an error
naming its session, not a skipped root), founds the fleet seated for the first,
and hatches each over its record replayed (AgentLog::resume(sessions_root, id); the name is live_roots’ alone),
the rest seated through the
launch’s genesis. It hands back one Resumed { turn, bytes } (record/session.rs)
per root, which the TUI’s tab notes after mirroring the log.
The two halves are Fleet::found(cfg, root_seat, installers, lease)
and Avatar::hatch(fleet, claim, seat, provider, open). found seats the first
engine (an identity seat through Genesis::seat; a wire seat is adopted as
built), reads the builtin index and $EXARCH_SCRATCH off it, substitutes the
scratch placeholder into the prompt template, and settles the Launch; it
refuses a fuelled wire root with no factory. hatch makes one root of that
fleet under the name its Claim holds (each caller claims first, then seats, then hatches, so a refused name boots no engine), resolves the prompt
(Launch::prompt_for), opens the log through open (which takes the prompt
as system: &str — the log records it as Protocol::System — and may hand a
fact of the opening back beside it, as resume does its Resumed), and assembles it
under the launch’s Trunk role, caps, fuel and search. The recorded
account is derived there, as Avatar::child_of derives it. RootConfig carries
the prompt, caps, fuel (exarch’s and synod’s launch sites pass SPAWN_FUEL),
how roots are driven (Trunk::{Headless, Attended, Embedded}), the Toolset
requests advertise (empty for --chat), the Egress
(exarch/src/egress.rs) whose search verdict is read once, and the optional
factory a wire root makes its children’s engines with
(below) — all of it ends up on the fleet’s Launch, held
once. RootSeat picks the seat kind: Identity states the cwd, terminal
capabilities and the run’s scratch, and becomes the launch’s Genesis; Wire
adopts a built transport whose engine lives elsewhere, and leaves genesis
None. The run’s advisory lock is the launcher’s to hold, never the node’s.
Egress bundles the two things a fleet’s outbound network shares across
every fork: net_policy::NetPolicy (exarch/src/net_policy.rs, the
allowlist read from /etc/exarch/net-policy.ral or the embedded default —
an exact hosts list of lowercase ASCII DNS names plus search, with
read/write, max-bytes and rate-per-minute refused as hard errors
naming their replacement) and an AuditLog (exarch/src/egress.rs),
reduced to one Tunnel record per attempt: its final vetted address, and on
close the byte count each direction carried — telemetry, not policy. None of
it is a model-facing verb — there is no fetch-url builtin
(builtins). The same Egress a trunk opens at launch
is what guest-net::Config::egress takes: the policy and ledger a synod
session’s guest network is gated by are the fleet’s own, not a second
copy (egress, synod). Host-mode exarch, which
has no guest to police, still keeps Egress for one thing: the search bit,
which every fork inherits verbatim. provider::wire::tool_defs attaches the
hosted search tool per request, only where the adapter has one, so a /model
swap or a role child provider never leaves a stale verdict behind.
A node built to converse — every attended or embedded root — withholds
reply; whether it also parks for a human is the launch’s say, not the
node’s. Two facts: the Role is per node, fixed at birth; attended is per run, fixed at launch — a human at the inbox (the
TUI), or none (synod drives one exchange at a time; headless seeds once).
Both behaviours fall out of construction and position, never an is_root
branch:
role(a) // fixed at birth: fork Child, root by how the run is driven (Headless or Converses)
attended // fixed at launch, on the fleet: Trunk::Attended alone
park_mode(a) = Quiesce if a.seat severed
Quiesce if role(a) = Converses ∧ attended ∧ a.token.terminated() // a shutdown signal reached it
Held if role(a) = Converses ∧ attended // immune to cancellation
Engaged if role(a) = Child ∧ (engaged ∨ a.has_reply()) // talked to, or holding a reply; a terminate cause still ends it
HeldByChildren if some live child of a is still awaited
UntilCancelled if a.schedules.armed()
Quiesce otherwise
role (agent.rs) is a construction-fixed field — one value read by
parking, the desk’s reply refusal (builtins), and
the per-agent resolver (agent/index.rs: BuiltinIndex, Grants, the verb
families and their sections; prompt.rs keeps only the shared base and its
placeholders, so prompt never depends on agent). The resolver applies the
index from the Role, the launch’s allow_schedule, and
spawns = fuel > 0, so no
agent is shown a verb or section the desk would certainly refuse; the nudge
layer, parking, reply availability, and advertised vocabulary cannot disagree
(reply-terminates-returning-agents).
park_mode (agent/attend.rs, returning a ParkMode of Held / Engaged /
HeldByChildren / UntilCancelled / Quiesce, bus/inbox.rs) is the should_park
verdict: a conversing node with a human attached parks Held, immune to
cancellation — unless its own token has been terminated (the one self-check
not answered by a Weak::upgrade), which a shutdown signal stamps, since a
conversing root has no parent to prune it away and parking Held past that
stamp would be a zombie; an unattended conversing root falls through to the
fleet arms, so it holds for live children and quiesces once they settle — the
exchange synod drives ends at fleet quiescence with no policy hook; a
returning, parented agent that is messageable — someone has messaged it, or
it holds a deposited reply — parks Engaged, the same wait except a
terminate-class cause still ends it, since an exchange is not a conversation;
busy children hold it until they reply or die; a live self-schedule holds it
until cancelled; otherwise it terminates at quiescence — the one-shot
contract a headless root satisfies. --chat builds the trunk with no system
prompt and no tool at all (an empty Toolset), so its Nudges steer
nothing — a bare conversation, the same attend loop.
The attend loop
Three nested loops, the same for root and child alike:
-
attend— the per-agent lifetime. The process hears the OS signals (signals::face(&fleet), held at each site that launches one —headless::run,headless::converse_settled, andtui::tui_loop::run— for the whole run), which forwards each to every live root’sAgent::interruptorAgent::cancel; a sub-agent hears none, being reached through the tree cascade (Agent::cancel_tree).attendand its bounded twinattend_backlog(converse’s per-exchange drain) are one loop,attend_until, differing in how an empty inbox is treated (Idle::Park/Idle::Return). Each pass draws the next item from this agent’s inbox — parked, vianext_or_idle(|engaged| self.park_mode(engaged), …), which re-evaluates the park verdict on everyCondvarwake, so the idle lease’s terminate-cause cancel, or the last live child settling, is seen on the very next wake — and hands it totake_up, which answers aStep { settled: Option<AgentOutcome>, flow: ControlFlow }or the engine’sSevered: every item reaching here already survived the inbox’s own pop-time fence, so what is left is the exchange-boundary latch reset, a session command’s run (Avatar::read,Avatar::rewrite), the item’sarrive(booked against the park verdict and announced,agent/arrival.rs, the same door a mid-exchange arrival takes), itsattempt(deliberateundercatch_unwind, the log quiesced however it ended),record_failure(a provider error recorded, a refusal’s resume armed), and the nudge reaction,follow_up. A genuine exchange boundary resets the nudge budget and clears the sticky cancel token (cancel::Token::reset), so a prior exchange’s Esc cannot bleed into the next; a self-nudge is the same exchange continuing and resets neither. The budget counts consecutive repairs, sodeliberatealso clears it after every completed tool round: faults scattered through a long headless run, which is one exchange, never add up to one that ends it. Everyreplydeposits its value on the agent itself (Agent::deposit_reply, which also posts the one-line notice to a parent); a child then parksEngaged, and only a parentless returning agent — the headless root — ends its loop on it, its driver reading the deposit offAgent::reply. The loop’s outcome is its last settlement; one that settled nothing ends on why it stopped drawing —Cancelledunder a terminate cause (a lease reap, acancel), a finish withoutreplyotherwise. At the single exit the loop winds a stranded prompt back throughquiesceso the nextappend_useris always admissible (exchange-ends-ready); a root’sAvatarthen drops (a child’s drops when its detached thread returns), pruned from its parent’schildrenand the fleet’s indices at their next walk — the whole of deregistration — so the fleet empties as its last agent’s avatar goes. A panicattemptcatches around itsdeliberatecall,Fault::Panicked, is a host-side fault (provider transport, surface decode, render, digest), recorded asAgentOutcome::Failed; an eval-side panic never unwinds this far — the engine’s own run door (Shell::run) checkpointsenv/contextat entry, rolls them back, and reports the failed run, durability being engine-owned (panic-recovery). The per-call host —RunHost, wrapping the desk and applier (shell-eval) — is never installed as shared state: it is a plainArcAvatar::ralbuilds and holds on its own stack, so a panic unwinding throughdeliberatedrops it with everything else there, with nothing separate to retire. -
deliberate— one prompt driven to quiescence over the agent’s own provider (self.provider.current(), read once byattemptso a/modelswap lands next item, never mid-deliberation), answeringResult<Outcome, Fault>whereFaultis the provider’s own error, the engine’sSevered, a log append refused, or a host-side panic. Both types live in the leafagent/outcome.rs, whichattendandnudgeread without depending on the loop.deliberateitself is a short driver: weigh the context for an eviction (must_force_evict), take oneround_trip, and act on theTurnit answers —Ends(Outcome),Calls, orRetryafter an overflow thatrelieveanswered with an eviction, never twice running.round_triprecords the turn’s start, renders the context and streams a reply throughprovider.complete;settle_turnrecords its usage, admits then appends the assistant message, and reads the turn’s verdict off its cut and its calls;answerruns the batch (run_batch), appends the results, takes in the mid-exchange arrivals and the newly climbed warnings as steering, and ends the deliberation on a cancel or a stagedreply. The admission step (admit_assistant, at the commit boundary) enforces the transcript-admission invariant: it repairs a non-object tool-callfn_argumentsto{}(X2) and substitutes a stub for an otherwise-empty assistant message (X7), so every committed message serialises to a request a strict backend accepts.StopReason::MaxTokenswith no captured tool call raisesProviderError::Truncatedafter appending the partial, so acontinuenudge keeps the work as context (a stall’s partial is prose, since reasoning alone is retried below the transcript, and its nudge says the connection failed rather than that the output was capped); with captured tool calls it dispatches them and continues instead, since returningTruncatedthere would strand the protocol inAwaitingToolResultsand fail the nudge’s nextappend_user(X6). Nothing bounds the number of round-trips: a deliberation ends when the model stops calling tools, or on a cancel or a provider error. -
A severed seat runs nothing more.
attendbreaks at the first severance it sees and every park quiesces behind one;run_batchanswers each call left in the batch with theEngineLostsentence, anddeliberateends the exchangeErr(Fault::Severed), whichtake_uppasses up as its ownErr— one?where there were eight polls.attend_untilrecords the loss once — the plain sentence as the failed outcome, the logged form (code and the engine’s own words) as a durable note — and headless and synod end the conversation on it. -
run_batch— runs one turn’s tool-call batch in order, each call throughinvoke. Every call returns aSessionToolResultsynchronously — dispatched through the launch’sToolset(ral, andthinkingunder--thinking-tool, tools); a spawn verb insideralhands the script a start receipt after launching the child. Once every requested tool id has a result,run_batchdrains this agent’s inbox’s tool-boundary steering. A non-slash steering prompt is appended after the complete tool-result batch, and the next turn asks the provider with the user’s steering in context (tool-boundary-steering). A sub-agent has no human writer, so its inbox holds no steering — but the channel is never idle for it either: the context-pressure reminder joins that same one admitted message (the-turn-is-the-atom).A streamed turn closes through one door,
commit::Stream::close: the stream keeps the first record that failed (a streaming callback has no error channel), seals the answer and reasoning choppers, then publishesTransient::Boundary, even when the provider or cancellation path already carries an error. On such an exit the close is best-effort (abandoned), so a live display edge cannot survive into the next turn (frontend).
Worker-reap and large-binding notices need no drain at attend’s top:
core’s own engine pushes both as `notice surface classes at the ready
boundary of the run that produced them
(engine-protocol), decoded by
shell-eval’s decode_surface into Surface::Notice
and recorded from there. A reap notice names a worker removed by policy — the
retention sweep expiring a settled entry’s unclaimed result, the one policy
that removes an entry — rather than one a claimer removed first. It is recorded as Forensic::Reap and drawn nowhere — a
worker nobody watched is not news — and never model-facing, since delivery of
a reap to the model itself is deferred.
attend’s top runs nothing else; the disk ceiling is weighed at the tool
boundary with the other gauges (below). (No pin is protected or reconciled —
pins.)
The retention clock itself is core’s: the engine ticks the worker registry
once per source dispatch and sweeps it at each settled run’s ready
boundary (shell-state), armed with
shell-eval’s SETTLED_WORKER_RETENTION. The
agent keeps no mirror of that drum: the engine answers the question itself,
each WorkerRow the `workers probe returns carrying retention_left —
the calls until a settled entry expires, on the engine’s own clock — which
/resources renders as the nearest expiry. Retention notices need no
plumbing of their own: they ride the same drain above.
The binding-lease ledger is armed by shell_eval::arm_session_ledgers —
the one policy site, run by the one root recipe (boot::engine_root) right after
the Root’s env is seeded as bindings (seeding then arming stay one visible
sequence); a fork keeps those terms through core’s Shell::bequeath and
Shell::child_of, sealing everything it inherited (shell-state) — with
shell-eval’s BINDING_IDLE_CALLS (256) and
LARGE_BINDING_BYTES (1 MiB), beside arm_worker_retention. The
large-binding residency nudge rides the pushed `notice channel above,
and the prune half is engine housekeeping too: idle top-level names fall at
the engine’s own ready boundary, announced as a pushed
`notice `prune [...] class (core’s Notice) the host records as Forensic::Prune
— kept in the log, drawn by no fold. The engine’s
run-entry checkpoint orders after any prior boundary’s prune, so a later
panic rollback can never resurrect a name a pass just pruned.
/resources is the probe fold over the same accumulators
(probe-convention): a Read drained at the
next boundary, tool or exchange, and run by Avatar::read on the attend
thread that owns the agent —
Avatar::resource_rows surveys what this thread may legally read — the worker
registry’s running/settled split with the nearest retention expiry and the
binding-ledger figures read as data over the transport’s probe rail
(Frame::Probe, through core’s typed Transport probe doors and Seat::read,
which severs the seat on a refused answer,
engine-protocol), plus inbox depth per
source, the event ledger’s logical length and history bytes, the log dir
walked host-side and the scratch sized engine-side (Transport::path_bytes) at
invocation, and the sub-agent idle lease as two rows (nearest
time-to-reap) — and emit_resources posts one Transient::Resources
carrying the rendered card — chrome only, so unlike a recorded observation
there is no later fold to re-render one from. A row’s policy is the
closed resources::Policy enum, rendered as it reads.
A probe fold is an interactive diagnostic, read when it is run: no session
keeps a pressure history, so the figures live only in this emission, never
in record.jsonl. The frontend appends the rows for the accumulators it
owns (scrollbacks, views, the bus, the demote threshold — tui/resources.rs)
at render time; neither half reaches across a thread, and the agent module
never names the TUI. Probing never mutates and never renews a lease —
enumeration is not observation — and the fold is never model-facing.
The inbox’s per-source depth is a probe figure, not a quota.
Mailbox/Inbox::push is infallible, one shared rule (Shared::push)
split by source: the three idempotent sources (user, schedule, nudge)
coalesce instead of growing the queue — a ScheduledWakeup replaces a
still-queued wakeup for the same schedule id (newest wins), consecutive
UserSteering pushes merge one per line (never across a slash line, which
would silently change its exchange-boundary classification), and a Nudge
replaces any still-queued nudge — newest wins, since a second means a
fresher continuation superseded the first, not that both are owed.
The other four (AgentResult, AgentMessage, Command, Surface) simply
queue, uncapped — the one exception to the long-session-budgets principle
that every accumulator needs a bound: none of them is machine-floodable — a
child or worker posts its result once, a slash command arrives at a human’s
typing rate, and a message costs its sender a model turn that fuel already
bounds — so a cap could fire only wrongly, and when it did the child would
park with its reply staged but its parent never notified, the very silent
loss a cap is meant to rule out.
There is no headless-completion gate and no role flag beside parent.
Nudging splits into two disciplines, named and owned by agent/nudge.rs’s
Nudges (Avatar::readings.nudges, which steers nothing for a toolless
--chat trunk): a
repair — Empty, Stopped, Truncated, or a returning agent’s
Complete without reply — is event-shaped, exceptional, and spends the
consecutive-repair budget (BUDGET, 3); a standing condition — the pin
register here, and the ladder conditions of agent/gauge.rs below — is a fact
about the agent’s own live state, edge-triggered: told once when it
changes, silent while it holds, budget-free.
The two kinds ride two channels, because they are owed at different
boundaries. react, the post-deliberation entry point, composes the repairs
and the pin reminder into at most one EXARCH_REMINDER message per
completion, self-posted as Post::Nudge and committed by append_user inside
the same exchange, gated on quiet — no standing reply, no running
worker, no busy children, the one condition those kinds share.
Nudges::remind is the other entry point, reached from deliberate’s tell at
a tool boundary, where a gauge’s Reminder — figures, not words; every
model-facing sentence is nudge.rs’s to word — lands as steering trailing
the batch’s arrivals: an agentic run takes one prompt and then two
hundred tool turns, so a warning that waited for the next completion would
arrive after the cut (the-turn-is-the-atom).
Its breadcrumb records no budget (Forensic::Nudge { cause, spent: None });
a repair’s records what it spent (spent: Some(Spent { used, max })).
| kind | channel | trigger | budget |
|---|---|---|---|
| empty-turn repair | react | quiet ∧ Ok(Empty) | spends |
| early-stop repair | react | quiet ∧ Ok(Stopped) | spends |
| truncation repair | react | quiet ∧ Err(Truncated) | spends |
| reply repair | react | quiet ∧ Ok(Complete) ∧ must_reply | spends |
| pin reminder | react | quiet ∧ Ok(Complete) ∧ register non-empty ∧ digest changed | free |
| pressure reminder | steering | gauge Over ∧ excursion untold | free |
| usage reminder | steering | an allowance window climbs past 90 or 95% | free |
The pressure reminder names the span it is warning about: the pressure
reading calls Avatar::planned_forced_eviction — a walk back over the frontier’s
own summed weights, no rendering — and carries the answer as
Pressure::Over(Load { used, total, trigger, unit, planned: Option<Span> }),
the one payload Reminder::Pressure hands on: figures only, unit tokens or
bytes, every sentence worded in nudge.rs. So the message tells the model the
trigger (the forced-eviction threshold, a reserve beyond the warning, in the
unit of the reading) and which turns a forced eviction
would take as things stand — Once it passes … a forced eviction will take the oldest part of your context; as things stand, that is turns 1–80, the live
prompt among them if it lies in the span. The eviction comes at a turn boundary
only once the context passes the trigger, possibly many turns later, and takes
the span planned at that moment. The message adds that the material stays
readable with exarch-transcript but a forced eviction leaves no note, and how
to make the cut with a line of its own —
exarch-context `evict [from: a, to: b, note: '…'] — before they go.
With nothing old enough to shed, planned is None and the message is the
reading plus the trigger. Durability is the log’s job, so the nudge
never asks the model to write state to files.
Everything else is accepted as-is, unspent, and deliberately so: Replied (a
reply is final), Cancelled (the human asked), and every unclassified
provider error (the transport’s own).
Nudges owns the repair budget used and the pin digest last told
(pinned_told); the ladder latches are gauge::Gauges’ (below), and the
two sit together with the token Measure in Avatar::readings — what the
avatar has measured and told about one context.
The pressure reading (gauge.rs’s Avatar::pressure) is a pure &self reading with no latch of its own, and
deliberate neither latches nor unlatches around it. An edge is consumed
at decide time, in the same act as the part’s emission, so “told but not
sent” and “sent but not told” have no spelling. reset(), called on every
exchange-opening item, clears only the budget — a new exchange is not a new
condition, and clearing the edges there would re-tell an unchanged pin once
per delivered item. Every path that discards a decided-but-uncommitted nudge
instead rebuilds Nudges whole: /clear (Inbox::clear sweeps the queue,
Avatar::clear rebirths the whole Readings) and /rewind (drop_nudges
sheds the queued nudge, rewind rebirths Nudges) — the rebirth also covers a /rewind typed mid-
deliberation, whose Barrier queues ahead of the nudge follow_up posts
after it: the nudge is decided (its edges already consumed) and then shed,
so without the rebirth the telling would be recorded but never committed.
Edge-triggering the pin reminder closes a livelock: a stationary register can
produce at most one nudge, where a bare per-completion reminder would let a
Complete, its own reminder, and the next Complete cycle forever while the
register sat unchanged. A
standing condition staying event-shaped on the wire looks like it should
strain the fold law
(context-is-a-projection’s “one
state, one fold”) — a condition that holds seems like it should not be
re-stated per turn — but it does not: the transcript is already persistent,
so a committed turn stands in every later render until an edit sheds it, and
re-stating the register on every turn was never persistence, only
redundancy. What must be recorded is the level’s transitions, and an edge —
“the register changed”, “pressure crossed the line” — is genuinely
event-shaped, so recording it as a committed user turn is honest, not a
workaround. The alternatives are strictly worse against the law: a
render-time seam re-injecting live state would break fold(log) == memo,
since the model’s view would stop being reproducible from the record; a
dedicated “standing condition” record class still has to enter the fold to
reach the model, so it is a user turn with a fancier name plus a new record
variant, fold arm, and admission rule — machinery for no semantic gain. An
eviction may carry a telling out of the context without harm either way: both
conditions self-heal regardless — pressure re-fires per excursion by
construction, the pin reminder re-fires on the next register change, and the
model can always exarch-pins `list.
A --chat trunk’s Nudges steers nothing (Nudges::new(false), the
launch’s Toolset being empty): every nudge steers the model toward a
tool it does not have, so no rule runs, no reminder fires, and nothing
synthetic ever joins the conversation. Its provider errors still reach the
human, since that report is the attend loop’s own step — record_failure emits
Forensic::ProviderError for whatever error the attempt carries before it
ever asks for a nudge, so react decides and nothing more.
Ladder conditions live in agent/gauge.rs: a reading climbs a ladder of
levels, each rung told once per excursion, and a fall below a rung re-arms it.
One Latch (latch.rs) serves every ladder: it keeps the level last weighed,
and climb answers the highest rung between that and the new level, so a first
reading past two rungs tells the higher alone; cross is the one-rung ladder
of a yes-or-no condition. A ladder is bare numbers, and who hears a rung is written
once, where its Warning is built. Every measurement the avatar takes of
itself lives in gauge.rs too: the token Measure, the eviction trigger and
plan, the scratch probe. Three ladders ride it; their latches are gathered in
gauge::Gauges, except the third’s user latches, which live in Rations:
| condition | reading | rungs | audience |
|---|---|---|---|
| context pressure | Avatar::pressure (Unknown leaves the latch) | over the soft line | model |
| disk ceiling | disk_warning, walked at most once per DISK_WALK_INTERVAL (a minute) | over disk_warn_bytes | user |
| usage allowance | Allowance::percent of Provider::allowances, per account and window | user 50, 75, 90, 95% (USER_HEARS); model 90, 95% (MODEL_HEARS) | user once per account; model once per agent |
Avatar::climbed weighs all three after every tool batch, and tell delivers
each climbed rung: a user line as a Forensic::SystemNote, a model
Reminder worded by Nudges::remind into the steering message — nothing for
a --chat trunk, which reaches no tool boundary anyway. The ration gauge reads nothing
itself: the read is the provider’s, spawned on the road every request takes
and shared by every agent on the account
(provider’s Rations). The user’s latches are
Rations’ too (Provider::climb_allowances, on USER_HEARS), so a rung is
told once per account by whichever agent climbs it first; the model’s are each
agent’s own gauge::ration::Ration, since each model needs its own reminder,
and its outcome sentence reads Avatar::resumes_at_reset — the trunk, under
a launch that resumes: paused until the reset and resumed, or refused until
it. The network is read once per account; the user hears once per account,
each conversation’s model once.
A refusal whose reset lies past the in-place wait ends as a provider error:
on the terminal trunk alone (resumes_at_reset), record_failure answers
ProviderError::Refused with resume_at, which replaces any provider-reset
schedule with a one-shot wakeup at the reset and notes the time to the user.
Headless, synod and every fork fail instead. Once one agent’s request is
refused, Provider::complete refuses every later request the hold covers
(the account for an allowance, one model for a rate) before sending
(provider-fault-recovery).
The Fleet
agent/fleet.rs’s Fleet is { names: Mutex<HashMap<String, Slot>>, roots: Mutex<Vec<Weak<Agent>>>, exits, departures, lease: Duration, launch: Launch }
— two indices (Slot is Claimed or Live(Weak<Agent>)), a mutex and
condvar that wake wait_empty, the idle-lease bound, and the Launch: what every node of one
run shares, fixed once — trunk (how every root is driven; trunk.attended()
is whether a human types into it), allow_schedule, resume_on_reset,
the Toolset, disk_warn_bytes, the factory, the Bureau, the prompt
template with the BuiltinIndex each node resolves it against, and the
recipe for a root: the invocation caps, fuel, search, sessions_root
and genesis. Held here
once rather than copied into every Agent, so a fork’s Build names only
what is its own. It is not the tree: the tree is
Role::Child/Agent::children, and every walk — the roster, the cancel
cascade, the scope check — runs there. names is the by-name door a spawn
claims identity at (Fleet::claim) and exarch-agents `message/`cancel/`read
resolve through (Fleet::resolve); roots holds every root session —
a root reports to nobody, so a walk from roots
(nearest_reap) is how the idle-lease scan reaches every live agent in the
run. The frontend reaches an agent through the fleet: a tab resolves its
name with Fleet::resolve and checks the id, never holding the Arc<Agent>
past one handler, and /quit and the signal forwarder walk Fleet::roots
(frontend). Both indices are Weak, and a lookup prunes what has
settled as it walks. FleetBus and the Arc<provider::Engine> are not
fields of Fleet — the frontend builds its own (FleetBus::session, one
emitter per root stamping that root’s own mailbox) and holds the engine handle
separately; what every node
shares through Fleet is only identity resolution and the lease.
- A root is born from
Genesis.Launch::genesis— installers, cwd, terminal capabilities, the run’sScratch— isSomeon an identity host andNoneon a wire host, which boots no further engine.Genesis::seatisSeat::root, so the first root, a/newor/branchroot and a resumed root each get a fresh engine the same way; the scratch is the run’s and outlives every root and every/clear.Launch::seat()is the door that refuses, in a sentence, a host with no genesis. - Two steps:
Fleet::claim, thenFleet::enrol. A name is claimed before any log or engine seat exists (by each root’s caller ahead ofAvatar::hatch, by the desk’s spawn path for a child), under the one lock a racing spawn’s own claim takes:check_name(NameMalformed, so a wire peer cannot go around it) and the taken test (NameTaken) are decided there, and the name is held asSlot::Claimed. TheClaimrides theBuildandenrolconsumes it, turning the slotLive, so no agent is enrolled under an unclaimed name; one dropped unconsumed gives its name back. The one remaining refusal,Unborn::SessionDead(the parent raced a cancel), is decided inenrol. A refused agent never appears in the fleet, however briefly: only after the name is live is the agent adopted into its parent’schildrenor pushed ontoroots. Birth arms no reply lease. - The fleet is alive while
rootsprunes to empty — the literal “dies when no active agents remain” (Fleet::is_empty;Fleet::wait_emptyblocks on it, woken byFleet::departed, whichspawn_async’s worker calls once its avatar has fully dropped). An agent leaves by its avatar dropping (quiescence, cancel, a non-reply finish); a conversing node stays until/quitor/closebecause it parks, and the process ends with its last root; a headless root leaves at quiescence; a replied sub-agent parks under its idle lease and leaves when it is reaped. There is no human-less daemon: nothing lingers without a present human, running work, a bounded lease, or a bounded self-schedule. - The follow-up lease starts at reply.
Agent::deposit_replyrecordsStatus::replied_atand callsFleet::lease_replyfor a child only. AfterAGENT_LEASE_IDLE(one hour),lease_firecancels the subtree withCancelCause::TimedOutonly if that reply’s timestamp still matches.deliberateclears it throughdisarm_reply_leasewhen model work resumes; read-only commands leave it alone.reply_sincealso checks the inbox exchange clock so a queued human or agent message protects the child before the loop takes it up. A later reply arms a fresh deadline; an old callback cannot reap that new waiting period. Working children and roots have no reply lease.nearest_reapreports only active reply leases. Reading a reply, focusing a tab, or probing resources renews nothing.
Cancellation cascades the subtree, across both layers
The single cascade serves the deliberate teardowns — exarch-agents `cancel, a
returning agent’s reply, and the /clear / /close subtree reaps — and it
runs over the agent tree itself, not a registry. Agent::name is its
identity — well-formed and unique among live agents, both enforced at
Fleet::claim (Unborn::NameMalformed/NameTaken; the trunk holds
TRUNK_NAME, agent/build.rs), which is where a name becomes identity and
so the one place a wire peer cannot go around. Both doors claim before they
fork a log or dial anything, and enrol takes the claim, so the check is
race-free and there is no second, advisory one. A name is also the handle exarch-agents `message/`cancel resolve by,
through Fleet::resolve — and then, for `cancel and `read alone,
the scope climb (Agent::descendant); a message needs no climb. The roster
(agent/roster.rs’s listing) walks the same tree instead — Agent::walk from
the reader’s own root — never the name map, yet lands on the same set: every
enrolled agent is at once named and placed, so what a model can see through
`list is exactly what it can reach through `message/`cancel.
Every Agent carries a strong parent, so the tree
is the spawn tree directly: Agent::cancel_tree cancels an agent and its
whole subtree, Agent::cancel_descendants abandons a returning agent’s
children, and Agent::forget reaps a subtree and forgets what it
was awaiting. The fence a late result or deferred surface batch must
survive is not here: Avatar::clear drains this agent’s own inbox,
and that drain is what bumps the clear-epoch a bus::Stamp-borne message
is judged against at its own pop. The epoch is per inbox, not per fleet,
and the envelope a worker carries is its reader’s by construction — a
/clear in one tab must not throw away work another tab is still waiting
on. Each cancelled node is
stopped across both layers:
its cooperative Token (read by deliberate between turns and raced by
the provider’s mid-stream cancel) and the eval layer through its own
reach: InterruptTarget — the cell holding its seat’s current ControlSender,
republished by Seat::clear, so it never goes stale — whose interrupt strikes
the dispatch in flight (even one still waiting on the engine lock,
cancellation) and whose terminate cancels the
engine’s durable root, under either carrier alike — and a ral eval already
in flight unwinds at the evaluator’s poll points instead of grinding to its
timeout_secs wall. No root carries a lease, so a terminate reaches a root only when the
process is ending or on its own /close. No engine faces the process’s signals itself: signals::face
forwards each to every live root
(cancel-is-a-watermark,
per-agent-eval-cancel,
cancellation). Esc / Ctrl-C, by contrast, are a
per-tab exchange interrupt, not a cascade: they stop only the focused agent’s
current exchange (Agent::interrupt, reached through the focused tab’s own
Weak), leaving its descendants running
(cancel-per-tab); the focused agent’s
sticky token is cleared at each exchange boundary (Token::reset).
Cancelling the durable root already reaches a cancelled node’s own ral
workers with no edge of its own: a worker’s cancel scope is a child of its
shell’s durable root, and every Scope::is_cancelled walks its
ancestors. What the cascade does not reach is a node that ends without ever
being cancelled — the ordinary reply/settle path, or the trunk’s own
end-of-attend exit, where the avatar simply drops. Avatar’s Drop
(agent/build.rs) closes that gap in one place: it clears its own armed
schedules unconditionally and records the session-ended bookend — the
underlying seat’s own teardown cancels the workers registered on its shell —
the same law clear already applies explicitly below, so a settled-but-never-cancelled
agent leaks neither
(residency, session-ledger).
The provider is per-agent and hot-swappable
ProviderHandle is owned by the Agent, not threaded through attend’s
parameters. attempt reads self.provider.current() once per item. /model swaps
the focused agent’s handle directly on the UI thread (resolved through
the fleet by id), so a
swap on one agent never disturbs another. fork seeds the child’s own handle
from the parent’s current provider (ProviderHandle::new(self.provider.current())),
so the child inherits the model in force at spawn and may diverge afterward.
A builtin spawn may say otherwise. exarch-agents `start’s model field
names `inherit or `role <Str>, and ExarchDesk::child_provider
reads it before the SeatKind split, so both arms share one resolution and
a refusal unwinds nothing — no adopted fork, no forked log, no engine made.
`inherit shares the parent’s Arc<Provider>, allocating nothing; a role
is the Arc<Provider> the human bound to that name at launch
(Launch::roles, provider), minted once with the
launch’s tuning and output cap. With no roles configured, or an unknown name,
the spawn is refused saying so. The agent names neither a provider nor a model,
and a Roles prompt section lists the roles to an agent that spawns, only when
any exist.
The one AgentLog::fork call site — Avatar::child_of, which the desk’s
start arm calls — hands the child log the live model and account rather than copying the parent log’s,
which were fixed at its session start: a child forked after a /model on the
parent’s tab records what it actually runs, not the pre-switch model. A root
hatched by /new or /branch likewise starts on the focused agent’s current
provider.
AgentLog authors Protocol and its own bookends and nothing else: its
protocol mutations go through one funnel that emits and then advances the model
fold, and every other fact (usage, nudges, errors, TurnStarted, session
end) is emitted by its worker through the agent’s Emitter, with
emit_or_report where a failure is a fault rather than an error. fork
(pub(crate)) takes the child’s system prompt and a Memory, and a mnemon
child’s log opens with SessionStarted, Inherited, System in that order;
the /new and /branch roots (spawn::hatch_root) pass the prompt they built
and, for /branch, the Inherited they computed.
Lifecycle: clear, close, evict, resume, hatch, fork
clear rebuilds the focused agent without carrying cancellation residue forward:
it drops the waiting inbox before the reboot, so a prompt typed during that
reboot belongs to the new context, then reboots the seat (Seat::clear) — an
identity root boots a fresh engine through IdentityTransport::boot from the
same Attach, so the scratch it names and the prompt that names it stay true,
onto the same interrupt target, and resets the escalation ladder; a wire
seat or an adopted fork has no recipe to reboot in place and answers /clear
with a sentence. It then has the agent forget — cancel
its subtree, drop what it was awaiting, clear its wakeups and its pins — and
records Protocol::Cleared through AgentLog::clear, in the same
record.jsonl and through the same seam: every turn is withdrawn from the
context, the system prompt stays, and the log keeps every record, so the cleared
turns stay readable through exarch-transcript. clear refuses while tool
results are outstanding, and a cleared child keeps its parentage. The frontend
learns of the clear from the record itself, its mirror stepping Delta::Cleared.
Clear then rebirths the avatar’s Readings whole, a rebuilt context having been
measured and told nothing. It touches no process-wide state: a /clear on
one tab is that tab’s. Replacing the
transport drops the outgoing shell, whose LocalState teardown cancels
every worker still registered on it — explicit destruction ends a worker
whatever owns it, with no host call site to forget
(shell-state). A worker settling after the cancel still tries to flush
its deferred done batch through the boundary it captured before the clear;
the same pop-time epoch fence (shell-eval) that
already drops a stale agent result drops that flush too, so no pre-clear
worker output survives into the rebuilt context. It is the focused agent’s,
not a fleet-wide reset.
--resume is a root-only lifecycle, Avatar::resume: it reopens every live
root of the run, each from its own record.jsonl (AgentLog::resume, which
accepts any root head, not session 0 alone), validating and replaying it,
quarantining only an unterminated crash fragment, then reopening the file for
append. The live model and provider are selected again, the freshly built
prompt is recorded as Protocol::System iff it differs from the one in force
(AgentLog::record_resumed), while each shell is
fresh and receives a note, imported as a Protocol::Import turn, describing the
bindings, workers, cwd, scratch, pins, and schedules that were not durable.
A mnemon root’s record, headed by Protocol::Inherited, folds back like any
other by reading its resident rows through the pointers the link carries, so a
resumed /branch root needs its source session’s record.jsonl; an unreadable
source refuses the resume, naming the file. Wire seats, chat runs and child
logs are refused rather than half-resumed; a headless --resume takes exactly
one live root and otherwise refuses, naming the count — the only place roots
are counted.
A session ends by one of two rewrites, each run at the exchange boundary.
Rewrite::Quit breaks the attend loop and nothing more, so a resume reopens
the root; the TUI posts it to every live root. Rewrite::Close cancels the
descendants, records Forensic::SessionClosed and then breaks, so the
session is gone for good; the TUI posts it to the focused agent alone.
SessionEnded is written on every drop and is not a close. The TUI
interrupts the agent before either, and on a returning child Close simply
ends it, the parent receiving the usual non-reply notice and a resume
ignoring the record.
evict sheds the older half of the context when pressure crosses the context
window’s reserve (gauge.rs’s forced_eviction_due — used tokens into the top 15%
of a known window; EVICT_THRESHOLD, 500 KiB of serialised history, is the
fallback when the window is unknown) and Context::can_evict holds (no
pending tool results). It is called at the top of every deliberate loop
iteration, before the TurnStarted record: every turn boundary is a boundary the
protocol is well-formed at, and the turn a cut would take is never the one in
hand, so there is no iteration where weighing is unsafe
(the-turn-is-the-atom). Long
autonomous and headless sessions therefore stay bounded within one
deliberation, without an interactive /evict.
suffix_keep_budget (half the history bytes) sets the cut and
plan_forced_eviction(keep) walks back over the frontier — resident turns and
resident residues — from the newest item, spending that budget; the
first item that does not fit sets to, and the cut is one prefix span
from the frontier’s first item to there, Option<Span>, None when
nothing is old enough. The newest item is never a candidate, so the
work in hand is never in a plan — the budget alone would cut everything when
the newest turn outweighs the whole of it. The live prompt is an item like
any other, so the span may take it; the model, warned first, keeps what
was asked only by evicting under its own note. The harness never
rewrites: a span that would be exactly one residue is no plan, since
rewriting it without a note would save nothing, while two or more items,
residues included, merge
(an-eviction-is-a-span). No provider call is
involved, so a boundary Esc has nothing to interrupt and simply leaves the
context as it lies
(esc-non-escalating-interrupt).
force_eviction applies the planned span as
evict_unbidden(span, EditAuthority::Harness), under one
Transient::State(AgentState::Evicting) for the state row — momentary, since
the edit is the whole of it — and answers whether it landed; relieve’s
once-only retry is unchanged. It records Protocol::Evicted { residue: Residue { span, note: None, by } }, the span being the resolved identity, so replay
re-derives nothing. The same fold step pushes that Residue and stamps
every resident turn and residue within its span as under it, moving the turns
to There, freeing their records and retaining only their byte ranges in
record.jsonl; this is residency following the context, not a second drain
rule. The residue renders (render_residue) as one user-voice message saying
the turns left in a forced eviction without a note and how to read them back.
Nothing old enough to evict is a no-op, not an event. The durable log is
appended to, never rewritten.
A forced eviction is one authority over the one context edit
(context-is-a-projection). The
model reaches the other two: exarch-context `survey/`evict [from, to, note] survey and edit the context and exarch-transcript’s `index/`read/`grep read the record, an edit recording
Protocol::Evicted with EditAuthority::Model rather than Harness; only the
model’s own `evict carries a note, and it is required. The user’s own hand is
/rewind, a picker over the prompts on screen (frontend),
which is not an eviction: AgentLog::rewind(anchor)
records Protocol::Rewound { anchor } alone, and the fold step withdraws the
anchor and every later turn — evicted ones included — from the context, leaving
no residue. The turns stay readable through exarch-transcript, the next
prompt is minted reach + 1 so ids stay monotone, the automaton rests where the
kept prefix leaves it (state::resting), and an anchor already withdrawn, or
strictly inside a resident residue’s span, is refused. Avatar::rewind sheds
queued self-nudges and rebuilds the nudge state; the screen learns of the rewind
from the record. The shell is not rewound,
and the picker says so. AgentLog::append_user(text, continues, voice) decides
once whether a prompt opens a turn (UserPrompt { turn, voice }) or extends the
live one (Steering), and the view fold draws the prompt off that very record
iff its Voice is not Harness, so the screen knows exactly which turn each
prompt opened (Item::voice: Human and Wakeup as themselves, a peer
Message as Peer, and a child result, nudge or surface notice as Harness);
/context surveys the context without editing it — emit_context_survey
posts the survey’s span rows as a Display::Context fact, and
context_rows_card groups them off their roles at draw time, a group opening
at each user-role row — a prompt (turn 12) or a residue (turns 12–40,
labelled by its note) — with the rows answering it and their weight — a Read beside
/resources and the session-opening /new and /branch
(agent/command.rs).
Avatar::disk_warning is the disk half of the same ADR (“Disk: report
and warn only”) — report-and-warn only, never rotation or deletion.
Unconfigured (config::disk_warn_bytes absent, the default) it is a no-op
by construction: no walk, no cost, ever. Configured, it walks at most once
per DISK_WALK_INTERVAL (a minute, stamped on the Gauges, so a /clear
walks afresh),
weighed at the tool boundary with the other gauges.
Crossing the ceiling (the session log dir, sized host-side by
path::tree::bytes, plus EXARCH_SCRATCH, read and sized in the engine by
Transport::env_var and Transport::path_bytes) is told to the user once, as a
Forensic::SystemNote, on the disk ladder’s Latch — one warning per
excursion, not one per boundary.
A fork builds the child Avatar (and, inside it, the child Agent) for
sub-agent spawning out of a fork the engine makes of itself
(Shell::bequeath, the flow matrix), never one
the host holds. Avatar::child_of, which the desk’s start arm calls, builds
the child; no Shell crosses into exarch. The child runs under its parent’s
GrantStack, and the one layer the spawn’s grant names
(`policy::base_layer` for a base tag, decode_capability_map
for a `restrict record, nothing for `inherit —
a-spawn-is-one-layer) rides the
Bequest and is pushed engine-side by Shell::child_of, through
SpawnGrant::narrow_onto, whether the child is adopted by
IdentityTransport::adopt_parked or sealed and made by the host’s factory.
A returning child sets parent: Some(..) — the strong tree edge that
routes its result and drives the subtree cascade — and enrols itself in the
fleet (Fleet::enrol), joining the shared Arc<Fleet> every node holds.
It snapshots the serialisable fragment of the parent’s lexical scope —
prelude, agent library, every accumulated binding — with its dynamic context
(cwd, env, grants, handlers) and the installed builtin table, and starts fresh
in everything else: fresh control counters and a freshly-defaulted
SessionState, so it holds no terminal authority (TerminalAccess::Denied,
no lease — a sub-agent is not the foreground agent and can never seize the
controlling terminal the TUI owns).
- The fork. The spawn builtin (
fork_then_enquire,shell_eval/builtins/harness.rs) takesShell::bequeath(grant), the one door both spawn arms take (agents’s one snapshot law): the identity arm parks theBequestand the handler redeems it withadopt_parked(id), the wire arm seals it (seed::seal) and names the bytes in its enquiry. The grant rides the bequest; the desk does not readspec.grant. The scrub replaces eachValue::Handlethe bequest reaches with an`opaqueplaceholder — in a session binding, in any scope a closure captured, in a native’sappliedarguments, in a handler frame’s arms — the name staying bound, and a bequest carries no hooks (the flow matrix). Closures stay live, and a parent that reaches no handle bequeaths its scope shared, not copied. - The lease.
Shell::constructarms the child’s ledger from the bequest’s terms, keeping the parent’s lease terms and sealing every inherited name as baseline on either seat: inherited scratch is never pruned in the child (fork_child_inherited_scratch_is_baseline). There is no flow-back: the child’scd, env, and new bindings die with it. An agent with fuel left may spawn, and each fork hands the child one less unit offuelthan the parent holds (SPAWN_FUEL = 3at a root; the parent’s own fuel is never debited, so fuel bounds depth, not fan-out). Atfuel == 0the prompt drops the spawn family —exarch-agents— and the desk refusesexarch-agents `startwith the exhaustion text; the desk remains the runtime wall (spawn-fuel-ceiling) — so a delegation chain bottoms out by refusal a fixed number of generations down. The fork mirrors on the bus asSignal::Born— sent byspawn_asyncminting the child’s emitter, before its worker thread exists, so it precedes everything the child sends — andTransient::Died, regardless of remaining fuel; a worker thread that cannot be spawned sends theDieditself.
/new and /branch take no fork at all: a root is not a child. Read::Hatch { memory, name }, run by Avatar::read, calls spawn::spawn_root, which mints
the name (the given one, else root-{N} off one process counter),
claims it, seats a new engine through Launch::seat and hatches the root on the focused
agent’s current provider (hatch_root). The log opens as AgentLog::root, and
for Mnemon imports the focused agent’s inherited_context(), the same import
a mnemon spawn makes, with a fresh shell. The root then goes to spawn_async
with no prompt, so it parks for the human; the focused tab’s rail shows one
/new or /branch row. There is no engine-side branch door: no internal
builtin, no desk tag, so a model cannot open a session. A builtin spawn’s
`start tag leaves its fork the other way, and the desk’s
exarch-agents `start arm collects it and assembles it at one less unit
of fuel (builtins).
Prompt resolution is one function, Launch::prompt_for, shared by every
root (Avatar::hatch) and child (Avatar::child_of, either seat). The launch
keeps the unresolved base and the index; each node
applies its own Reply and child-fuel bits beside the launch’s
allow_schedule; the resolver appends Agents iff fuel remains, Spawned
(data/spawned.md: the snapshot, the spawner binding the reply, follow-ups)
iff ToSpawner, and Reply (data/reply.md, true of any receiver) iff the
agent returns. The headless root thus never reads that it was spawned. The
child’s log bookend records that fully resolved prompt length, including the
filtered index and late sections.
Wire-seat spawn
Both seats spawn in one exchange of the same exarch-agents `start
vocabulary; they differ only in where the fork waits. The arm is chosen on a
stated fact — HostServices::kind, a SeatKind::{Identity(parent transport), Wire} read off the seat when the desk’s capture is built, once per ral call
(agent/shell.rs), matched against the receipt the enquiry carries
(ExarchDesk::fork_seat). What varies is the fork tag the
engine mints beside the model’s own spec record, since the reentrancy law
bars a desk handler from holding the &mut Shell a fork needs:
- Identity. The run’s
Forkdoor isFork::Park(nursery), so the builtin body (fork_then_enquire) parks a scrubbed fork armed with the session ledgers and names the slot as`parked <id>. The desk adopts it by id through the parent’s own transport (IdentityTransport::adopt_parked), in the same process. - Wire. A guest engine’s runs carry
Fork::Seal(core/src/engine.rs): the builtin body does no I/O. It takesShell::bequeath,seals the bequest into bytes the host never reads, and the enquiry names them as`sealed <bytes>. No token, no ack, no seed fd and no table of children exist: no engine spawns an engine.
The desk’s wire arm (make_engine, desk/agents.rs) asks the launch’s
Factory (exarch/src/agent/factory.rs — vm_manager-free by
construction, a capability object RootConfig hands to the Launch, None on
every identity trunk) for one connection to a fresh engine, adopts it as a
WireTransport, and seats it with Seat::wire(transport, Attach::child(engine, seal)):
the parent’s EngineSpec plus the seal, written together, then await_attached.
The seat stores only the EngineSpec, so no seat holds its parent’s
bequest for its own life and a child attach has no cwd or env to get stale. The desk answers the parent’s enquiry only after that
returns, so the child is attached before the roster names it, and hands the
seat to the same spawn_async an identity fork reaches, at fuel = parent - 1.
A refused attach reaches the parent as the desk’s refusal, in the engine’s
words. Synod’s factory is the guest daemon on FACTORY_PORT, which admits only
VMADDR_CID_HOST and spawns one engine per connection (synod);
the child unseals against its own Runtime in Engine::boot.
One exchange, one connection: the whole of failure is local. A factory that
cannot make an engine refuses in a sentence; past the attach a refusal simply
drops the stream, the child reads EOF on fd 3, exits, and the daemon reaps it.
A wire root with fuel > 0 and no factory is a construction error, refused at
Fleet::found with a sentence rather than discovered by a model calling
agent; on a backend that cannot dial inward (the HCS machine) the factory’s
refusal says so in one sentence.
The enquiring builtin cannot tell which arm served it — both answer the same
roster exarch-agents gives every tag. See agents for the
seed’s isolation law and synod for MachineFactory, the
Factory over Machine::connect_guest. The `mnemon memory mode
additionally forks
the parent’s AgentLog and imports its model-visible context before assembly,
as /branch does for a root
(subagent-memory-modes); AgentLog
drops a pending unanswered assistant tool-call frame when the parent is
mid-dispatch, so the child inherits a request context rather than a dangling
provider protocol. Both memory modes seed the launch prompt through the
child’s inbox, so the prompt enters through the same item path.
Routing the fork through core matters because the builtin table is the easiest
thing to drop. The exarch host builtins — view-text/view-hash, grep-files,
edit-hash, edit-replace, explore-dir, find-files, the skill loaders
(builtins) — live in the agent’s own dispatch table
rather than in the environment, and the
view-text-around helper in agent.ral calls view-text. A fork that copied
only the scope and the context would leave the child’s view-text-around
resolving to nothing and falling through to a failed PATH lookup.
A session fork snapshots scope, context, and the builtin table as part of the
flow matrix, so the decision lives in one place and the table cannot be
silently severed at this call site.
digest.rs holds the fixed per-section byte caps for what the
model sees in history: each tool-result section has its own cap
(VALUE_CAP 20 KiB, STDOUT_CAP/STDERR_CAP 10 KiB), applied with clip from
shell_eval/clip.rs; the separate cap for opaque error blobs (OPAQUE_CAP)
lives with shell_eval/report.rs, whose rejection/fault composition uses it.
clip measures what a terminal would
show (ral_core::ansi::visible, replaying \r and backspace beside the
escape strip). The whole-history gauges — the EVICT_THRESHOLD byte
fallback, forced_eviction_due/eviction_trigger (the reserve line and the token
count that names it, for /resources), pressure_due and its
PRESSURE_THRESHOLD_FALLBACK one reserve ahead of it, and
suffix_keep_budget, the half-the-bytes cut — are gauge.rs’s. These caps
bound a single result; eviction bounds the whole history. A child’s reply is
not clipped, since it reaches the parent as a value through
exarch-agents `read rather than as text. An oversize section keeps a head+tail digest
and elides the middle, with a banner nudging the model to scope the query at
its source and re-read in slices — the same rendering the transcript records,
so the user never sees more of a result than the model does. A structured
VALUE arrives pre-budgeted — the printer spends its own byte budget inside
the value, cutting where a container can still name what it dropped — so clip
is a backstop there, while stdout, stderr, and raw payload strings still meet it
head-on.
Every ral result closes with a TURN: <id> line naming the turn it closes.
That is how the model learns its own turn number, and so the address it evicts
and reads by: the id exarch-context `evict [from: …, to: …] and
exarch-transcript `read [turns: …] take. It is appended in agent/shell.rs,
from AgentLog::current_turn(), not in digest’s rendering — the id is the
session’s, known where the result is assembled and not inside a byte-capped
section, and system.md tells the model to expect it
(an-eviction-is-a-set-of-turns).
Avatar::ral here threads to shell-eval’s
run_shell, answering an Evaluated { text, failed }; its desk capture
(HostServices) is built from the avatar alone, with no probe per call.
See also
agents (the role model these nodes realise — one parent
predicate, the conversing trunk vs returning agents),
tools (the Toolset a request advertises — ral, and
thinking under --thinking-tool) and builtins (the harness verbs the
desk answers), frontend (the bus, the inbox, the
fleet doors, and the two frontends), provider,
policy, exarch,
residency (the resident ledger this cascade and the
worker/schedule teardown edge are chapters of),
engine-protocol (why a wire
seat is one engine process, one connection),
agent-and-avatar (why Agent and
Avatar split this way, what the three-copy shape it replaced deleted, and
the later amendment that moved the run’s settings onto the fleet’s Launch),
the-turn-is-the-atom (the turn as
the unit of eviction, the loop-top weighing, and the pressure reminder’s
channel),
an-eviction-is-a-set-of-turns
(the one edit and its three authorities),
an-eviction-is-a-span (the residue an eviction leaves, and the pressure plan’s
one prefix span),
synod (MachineFactory, and synod’s own helper surface built over
the wire-seat spawn above).