JavaScript Executor & Socket Reactor
How the process-wide Tokio runtime, bounded socket tasks, durable readiness state, capacity-one VM wakes, and thread-affine V8 executor cooperate without running guest JavaScript on Tokio.
This page is an internals deep-dive on the boundary between the trusted sidecar reactor and the untrusted JavaScript executor. It explains which work runs as a Tokio task, how a socket makes the V8 executor runnable, which channels carry data, and how Node stream backpressure reaches the actual transport.
For socket ownership, loopback, DNS, and network policy, see Networking. For the client-facing JavaScript environment, see JavaScript Runtime.
Tokio never invokes guest JavaScript from a runtime worker. A Tokio task stores bounded work and publishes durable readiness. The separate V8 executor thread notices a capacity-one wake and enters its own isolate.
Process and thread topology
The sidecar process owns one fixed-worker, multithreaded Tokio runtime. Every VM and subsystem shares it. Socket readers and writers, listeners, UDP, TLS, HTTP/2, DNS, timers, protocol work, and lightweight VM supervisors are async tasks scheduled on those workers.
A Tokio task is not a dedicated thread. It is a future that may be polled by different runtime workers over its lifetime. A sidecar may have thousands of admitted handle tasks without creating thousands of OS threads.
Guest V8 execution is deliberately different:
- Each active JavaScript session receives an admitted, thread-affine V8 executor.
- The executor is a real OS thread outside Tokio. It owns the isolate and is the only non-V8 platform thread that enters that isolate.
- Synchronous guest JavaScript or a synchronous bridge wait can block that executor, but cannot occupy a Tokio worker or another VM’s executor.
- The number of active and warm executor threads is bounded separately from socket and task counts.
There is therefore no “Tokio task running a Node.js process.” Trusted I/O runs as Tokio tasks; untrusted JavaScript runs on a V8 executor thread.
One TCP read, end to end
The sequence has two paths that must not be conflated:
- The wake path carries only enough information to make the correct V8 executor inspect durable state.
- The data path retains charged bytes in the kernel or a bounded socket completion queue until JavaScript explicitly drains them.
1. The OS wakes a Tokio task
On Linux, Tokio’s mio driver registers the socket with epoll; other
platforms use their equivalent readiness mechanism. When the descriptor
becomes readable, Tokio schedules the socket reader future on a runtime worker.
Readiness means that a bounded nonblocking read is likely to make progress. It
does not contain the bytes.
The reader acquires its fairness turn, reads at most its byte quantum, reserves
accounting for the result, and sends a Data, EOF, or error record into the
socket’s bounded completion queue. If that queue or its byte budget is full,
the reader stops making transport progress. Remaining bytes stay in the kernel,
and TCP flow control eventually slows the peer.
2. The task publishes durable readiness
After storing deliverable work, the task publishes a flag such as READABLE
for the socket’s opaque capability ID and generation. The VM-scoped
ReadyState owns one bounded map entry per admitted capability:
capability ID -> {
capability generation,
ready flags,
revision,
application read interest
}
Publishing merges the flag into that entry and increments its revision. It does not append a readiness event.
If the VM has no wake outstanding, the broker allocates a wake epoch and sends
one tiny ReadyWake { generation, epoch }. The implementation stages it
through a capacity-one Tokio MPSC and immediately transfers it into the V8
executor’s capacity-one crossbeam lane. While that wake is queued or in flight,
later publications only update durable state.
The wake contains no socket bytes, capability list, packet count, or revision history. It is a doorbell.
3. The V8 executor notices the wake
When the executor has no JavaScript work to run, its blocking crossbeam selector waits on separate lanes for:
- a readiness wake;
- ordinary bounded session commands;
- registered bridge completions;
- abort and shutdown control.
The readiness message makes the V8 executor thread runnable. No Tokio worker
enters V8. If guest JavaScript is currently executing a long synchronous loop,
the wake remains outstanding and ReadyState continues coalescing work until
the executor can take another turn—matching Node’s rule that I/O callbacks do
not interrupt synchronous JavaScript.
4. Rust calls the guest dispatcher
On the V8 executor thread, Rust calls take_batch(epoch) on the readiness
broker. This is the implementation of the conceptual ready_batch operation:
it snapshots at most the configured work quantum from durable state. It is not
a JavaScript function and is not a queue of all publications.
The executor then enters its isolate and calls the guest global:
_agentOSReadyDispatch(capabilityId, capabilityGeneration, flags)
The bridge keeps a JavaScript map from (capability ID, generation) to a
readiness target. For a NetSocket, the target records a pending read wake and
queues one JavaScript microtask for the socket read pump. Generation is part of
the key, so a late wake for a destroyed socket cannot target a replacement
capability that reused its numeric ID.
5. JavaScript drains the actual bytes
The socket pump invokes the bounded raw net.socket_read bridge operation.
That operation drains the socket completion queue and returns bytes through
the call’s registered response target. A synchronous form blocks only this
VM’s executor while it waits; it does not scan or defer ordinary session
events and does not block Tokio.
The bridge hands returned bytes to Node’s Readable implementation:
Readable.push(bytes)
Readable is the readable half of the guest’s real Duplex socket stream.
push() adds bytes to its JavaScript-side input buffer and returns whether
that buffer remains below its high-water mark.
After the bounded turn, the executor calls complete_batch() with the
revisions it observed. This is the implementation of the conceptual
complete_wake operation. An acknowledgement clears a flag only if the
entry’s revision is unchanged; newer work cannot be erased by an older batch.
Revisions and wake epochs
Revisions and epochs solve different races:
- A revision belongs to one capability entry. It changes when that capability publishes newer deliverable state or its readable state is invalidated.
- An epoch belongs to one VM wake cycle. It identifies the single wake currently queued or being processed.
Suppose V8 snapshots socket 42 at revision 9. Before it acknowledges the batch,
the socket task publishes more data and advances the entry to revision 10.
The revision-9 acknowledgement no longer matches, so it cannot clear
READABLE. Completing the old epoch atomically observes that work remains and
queues one replacement epoch.
There is no revision queue. Revisions 8, 9, and 10 are successive values of one
integer in one bounded map entry. Likewise, READABLE is one level bit rather
than one message per packet.
Backpressure reaches the transport
If Readable.push(bytes) returns false, the guest buffer has reached its
high-water mark. The socket bridge:
- clears application read demand;
- sends
SetReadInterest(false)to the capability; - suppresses or clears deliverable
READABLEstate; and - leaves the Tokio reader waiting instead of repeatedly polling a level-readable descriptor.
Already admitted bytes remain in charged completion state. Additional bytes remain in the OS receive buffer, and TCP applies pressure to the sender.
When the consumer drains the JavaScript buffer, Node calls the socket’s
_read() method. _read() is a standard Readable hook: it means the stream
wants more input. The bridge sends SetReadInterest(true), wakes the reader,
and republishes readiness if source work is already known.
ref() and unref() are unrelated to this mechanism. They affect whether the
socket keeps the JavaScript execution alive; they never pause I/O or suppress
callbacks. Only stream demand controls application read interest.
Channel map
| Path | Primitive | What it carries | Bound and full behavior |
|---|---|---|---|
| OS to socket task | Tokio readiness registration | A readiness edge/level | No payload queue; task performs a bounded turn |
| Socket reader to capability | Bounded async completion channel | Charged Data bytes, EOF, error, close | Count and bytes bounded; full pauses the reader |
| Socket writer commands | Bounded Tokio MPSC plus reservations | Ordered writes, shutdown, options | Safe admission waits or returns a typed overload |
| Capability readiness | Revisioned map | Latest flags per capability | Cardinality bounded by admitted capabilities |
| Broker wake staging | Tokio MPSC | { generation, epoch } | Capacity one; repeated readiness coalesces |
| Executor wake | Crossbeam channel | { generation, epoch } | Capacity one; selected by the V8 thread |
| Session commands | Crossbeam channel | Ordinary session work | Configured bound; not used for readiness payloads |
| Bridge result | Call registry and call-specific target | One registered result and reserved bytes | Direct settlement; never scans the session event lane |
| Abort/shutdown | Dedicated control lane | Cancellation or termination | Reserved separately from ordinary data |
An MPSC capacity alone is not the backpressure design. Correctness comes from combining bounded channels with durable state, byte reservations, source pausing, call-specific response routing, and an explicit action for every full condition.
Why this avoids deferred-event exhaustion
The earlier synchronous bridge could wait for one host response while reading a shared session-event stream. Every unrelated event encountered during that wait had to be deferred. Once 256 unrelated messages accumulated, the synchronous bridge failed even though the host tool response itself was successful.
The reactor removes that dependency cycle:
- A registered bridge response settles its call-specific waiter directly.
- Socket readiness merges into bounded
ReadyStateand one wake instead of entering the ordinary event stream. - Socket bytes live in a separately bounded completion queue.
- Abort and shutdown use reserved control paths.
Consequently, a flood of ordinary session updates cannot sit in front of the response that a synchronous call needs, and a hot socket cannot manufacture one cross-boundary event per packet.
Relationship to Node.js
AgentOS copies Node’s evented-I/O invariants, not its process topology.
| Concern | Node.js | AgentOS |
|---|---|---|
| I/O runtime | One libuv loop per Node process | One Tokio runtime shared by the sidecar process |
| JavaScript execution | Event-loop thread enters V8 | Separate admitted V8 executor thread enters its isolate |
| Descriptor ownership | Node owns native descriptors | Trusted sidecar owns descriptors; guest sees opaque capabilities |
| Readiness callback | libuv calls the stream binding directly | Durable state plus one coalesced wake crosses the security boundary |
| Read backpressure | push(false) stops uv_read_start activity | push(false) disables capability application-read interest |
| Resume | _read() restarts native reads | _read() restores sidecar read interest |
| Writes | libuv completion settles callbacks | One ordered per-socket bridge tail settles callbacks |
| Liveness | Referenced handles keep the loop alive | Referenced guest handles keep the execution alive |
The extra readiness broker and bridge drain exist because the guest cannot own host descriptors or let a trusted Tokio worker enter untrusted V8. They preserve Node behavior across that security boundary.
Implementation guide
The main pieces are:
crates/runtime/src/readiness.rs: revisionedReadyState, wake epochs, and interest gating.crates/v8-runtime/src/session.rs: the executor’s bounded selector, readiness batching, dispatch, acknowledgement, and executor admission.crates/v8-runtime/src/stream.rs: the Rust-to-V8_agentOSReadyDispatch()call.packages/build-tools/bridge-src/builtins/readiness.ts: the guest capability target map.packages/build-tools/bridge-src/builtins/net.ts:NetSocket, the readiness-driven read pump,Duplexbackpressure, liveness, and ordered writes.crates/native-sidecar/src/execution/network/: sidecar-owned network tasks, bounded completion state, and transport operations.
See also
- Networking: kernel sockets, loopback, DNS, and policy enforcement.
- Processes: how V8 executions fit into the virtual process model.
- Limits & Observability: resource classes, queue bounds, warnings, and typed overload failures.
- Security Model: why the sidecar owns capabilities and the executor is untrusted.