A worker’s surface is a second channel, scoped to the session
surface speaks on two channels, told apart by the one thing a value’s class
cannot carry — the lifetime of whoever receives it. A foreground run surfaces
on the live channel, scoped to its dispatch and ordered before its Report. A
detached worker outlives that dispatch, so it surfaces on the deferred
channel, scoped to the session: it buffers, and at completion delivers the batch
with a closing `done record, which the host renders at its next boundary.
One vocabulary and one decoder; two channels, because there are two scopes.
Read as effects, a surface operation is handled by the innermost host handler
whose lifetime contains its producer: the dispatch for a run, the session for a
worker. A new kind of event is a class on the channel whose scope fits it; only
a new scope makes a new channel (engine-protocol,
asynchronous-effects).
Context
spawnreturns a handle and its worker runs on, past theReportof the run that spawned it (concurrency-detached-vs-structured).- A worker must not hold the live sink: a card arriving after the
Reportwould splice into sealed scrollback (watch-repl-builtin, session-lifetime-event-bus). - Yet a worker’s surfaced cards, and the fact of its completion, must reach the host without anyone polling for them.
Decision
- Two channels, one vocabulary. The live channel is
Event::Surface, inside a dispatch’s window. The deferred channel isSessionEvent::DeferredSurface, outside any dispatch, received by the host’sDeferredSink. Both carryFOValues and both are decoded by the same function, so a card renders the same on either, only later on the second. - The worker buffers and delivers once. Its mooring’s surface is a
DeferredSurface: a bounded buffer (an overflow marker past the cap) over the session’s sink. AFlushGuarddelivers the buffer plus aDoneEvent{id, cmd, outcome}on every exit — return, raise, or panic. - Delivery precedes the result. The flush runs before the worker sends its
result, so no eliminator observes a handle whose batch is undelivered.
await,raceandpollyield values and render nothing. - No sink, no surface. A mooring without a deferred sink gives its workers no surface at all; what they would have surfaced is dropped.
- Nesting inherits the scope.
Mooring::for_workercarries the session sink into every worker, so a nestedspawndelivers at its own completion. watchstreams on the same channel. Each line leaves as a one-value`watchdelivery as it arrives — still deferred, still rendered at a boundary.
Host side (exarch)
InboxDeferredposts each batch to the root session’s inbox as aStamped::Surface, under aStampminted when the sink is built.- A
/clearbumps the inbox’s epoch; the pop drops any post stamped before it, so a batch from a pre-/clearworker never renders. Avatar::arrivedecodes the batch into the root scrollback exactly as a live run’s values are, and the item wakes the model with the settled outcome —errand #7 finished (exit 0) · … Await its handle for the value.
Safety
- By type, not by rule. A worker’s mooring has no live sink and no desk
(
Mooring::for_worker); it cannot render now or enquire, because nothing it holds can. - A late card cannot splice. The deferred channel renders only at a boundary, as fresh scrollback.
- Data, not handles. A batch is first-order, so it crosses the wire and
lands in
events.jsonlwithout special cases.
Alternatives rejected
- A control class on the live channel, carrying the live handle to an exarch waiter thread (surface-carries-control): a handle in the payload breaks first-order transport, and a second observer of the handle needs a shared settle.
- Replay on
awaitinto the awaiting run, arbitrated against the flush by a shared latch. The flush precedes the result, so the replay could never win, and winning would have bought only rendering one frame earlier at the cost of a cross-thread test-and-set. - Deferring the foreground too. The foreground is the live frame; deferring it loses intra-turn progress.
Where
core/src/types/mooring.rs—DeferredSink,Mooring::for_worker.core/src/types/handle/surface.rs—DeferredSurface,FlushGuard.core/src/types/shell/workers.rs—DoneEvent.core/src/builtins/concurrency/birth.rs—spawn_child, the flush-then-send order.core/src/protocol.rs—SessionEvent::DeferredSurface; carried bycore/src/carrier/wire/{serve,front}.rs.exarch/src/shell_eval.rs—InboxDeferred;exarch/src/agent/arrival.rs—Avatar::arrive;exarch/src/bus/post.rs— the waking notice.