# JavaScript Executor & Socket Reactor

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](/agentos/docs/architecture/networking). For the client-facing JavaScript
environment, see [JavaScript](/agentos/docs/javascript).

> **NOTE:** 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.](/images/architecture/javascript-executor-wakeup-sequence-dark.svg)](/images/architecture/javascript-executor-wakeup-sequence-dark.svg)

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:

```text
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:

```text
_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:

```text
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.](/images/architecture/javascript-executor-readiness-state-dark.svg)](/images/architecture/javascript-executor-readiness-state-dark.svg)

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

| 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 `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.

| 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`: 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](/agentos/docs/architecture/networking): kernel sockets, loopback, DNS,
  and policy enforcement.
- [Processes](/agentos/docs/architecture/processes): how V8 executions fit into the
  virtual process model.
- [Limits & Observability](/agentos/docs/architecture/limits-and-observability):
  resource classes, queue bounds, warnings, and typed overload failures.
- [Security Model](/agentos/docs/security-model): why the sidecar owns capabilities and
  the executor is untrusted.
