Skip to main content
Advanced

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

Sequence from OS readiness through Tokio, durable readiness, the V8 executor, and the guest JavaScript socket read pump.

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

State machine showing repeated readiness coalescing, revision-safe acknowledgements, replacement wake epochs, and stream backpressure.

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:

  1. clears application read demand;
  2. sends SetReadInterest(false) to the capability;
  3. suppresses or clears deliverable READABLE state; and
  4. 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

PathPrimitiveWhat it carriesBound and full behavior
OS to socket taskTokio readiness registrationA readiness edge/levelNo payload queue; task performs a bounded turn
Socket reader to capabilityBounded async completion channelCharged Data bytes, EOF, error, closeCount and bytes bounded; full pauses the reader
Socket writer commandsBounded Tokio MPSC plus reservationsOrdered writes, shutdown, optionsSafe admission waits or returns a typed overload
Capability readinessRevisioned mapLatest flags per capabilityCardinality bounded by admitted capabilities
Broker wake stagingTokio MPSC{ generation, epoch }Capacity one; repeated readiness coalesces
Executor wakeCrossbeam channel{ generation, epoch }Capacity one; selected by the V8 thread
Session commandsCrossbeam channelOrdinary session workConfigured bound; not used for readiness payloads
Bridge resultCall registry and call-specific targetOne registered result and reserved bytesDirect settlement; never scans the session event lane
Abort/shutdownDedicated control laneCancellation or terminationReserved 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 ReadyState and 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.

ConcernNode.jsAgentOS
I/O runtimeOne libuv loop per Node processOne Tokio runtime shared by the sidecar process
JavaScript executionEvent-loop thread enters V8Separate admitted V8 executor thread enters its isolate
Descriptor ownershipNode owns native descriptorsTrusted sidecar owns descriptors; guest sees opaque capabilities
Readiness callbacklibuv calls the stream binding directlyDurable state plus one coalesced wake crosses the security boundary
Read backpressurepush(false) stops uv_read_start activitypush(false) disables capability application-read interest
Resume_read() restarts native reads_read() restores sidecar read interest
Writeslibuv completion settles callbacksOne ordered per-socket bridge tail settles callbacks
LivenessReferenced handles keep the loop aliveReferenced 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: revisioned ReadyState, 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, Duplex backpressure, 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.