# RemoteExecutor

Carry the SDK's remote Code Mode frames over a message socket.

`RemoteExecutor` implements `RuntimeOptions.remote` for one connection to a remote executor or a hand. It keeps frames opaque, enforces `max_frame_bytes`, keeps the first frame as the connection's catalog, and lets one runtime own the connection at a time. It works over any `MessageSocket`, such as a WebSocket.

```ts title="remote.ts"
import {
  RemoteExecutor,
  type RemotePolicy,
  type RuntimeOptions,
} from 'nanocode-wasm'

const policy: RemotePolicy = {
  max_frame_bytes: 1_048_576,
  timeout_ms: 10_000,
}

// 1. Dial the executor and wait for its catalog frame
const executor = await RemoteExecutor.connect(async () => {
  const socket = new WebSocket('wss://executor.example.com')
  await new Promise((resolve) =>
    socket.addEventListener('open', resolve, { once: true }),
  )
  return socket
}, policy)

// 2. Pass it to open with config.remote set to true
const remote: RuntimeOptions['remote'] = {
  cancel: (id) => executor.cancel(id),
  dispose: () => executor.dispose(),
  execute: (request, id) => executor.execute(request, id),
  failed: () => executor.failure !== undefined,
}
```

## Signature

```ts
class RemoteExecutor<S extends MessageSocket = MessageSocket> {
  static connect<S extends MessageSocket>(
    open: (signal: AbortSignal) => Promise<S>,
    policy: RemotePolicy,
  ): Promise<RemoteExecutor<S>>
  static accept<S extends MessageSocket>(
    socket: S,
    policy: RemotePolicy,
  ): RemoteExecutor<S>
  static restore<S extends MessageSocket>(
    socket: S,
    policy: RemotePolicy,
    catalog: string,
  ): RemoteExecutor<S>
}

type RemotePolicy = { max_frame_bytes: number; timeout_ms: number }
```

## Parameters

### policy.max_frame_bytes

* **Type:** `number`

The largest frame in UTF-8 bytes that the executor accepts. A larger incoming frame closes the connection with code 1009. For outgoing frames, the SDK uses the smaller of this value and the limit in the hand's catalog, and reports an oversized batch as not dispatched.

```ts
import type { RemotePolicy } from 'nanocode-wasm'

// Accepts frames up to 1 MiB
const policy: RemotePolicy = {
  max_frame_bytes: 1_048_576,
  timeout_ms: 10_000,
}
```

### policy.timeout_ms

* **Type:** `number`

The deadline in milliseconds for `connect` to dial and receive the catalog, and for `ready` to receive it. Both values must be positive integers.

```ts
import type { RemotePolicy } from 'nanocode-wasm'

// Gives a slow hand 30 seconds to send its catalog
const policy: RemotePolicy = {
  max_frame_bytes: 1_048_576,
  timeout_ms: 30_000,
}
```

## Constructors

### connect

```ts
// Dials, listens, and waits for the catalog within timeout_ms
static connect<S extends MessageSocket>(
  open: (signal: AbortSignal) => Promise<S>,
  policy: RemotePolicy,
): Promise<RemoteExecutor<S>>
```

Calls `open` and listens on the socket for `message`, `close`, and `error`. It resolves after the first frame arrives. If the deadline elapses, `signal` aborts and the promise rejects with `remote executor connection deadline elapsed`.

```ts
import { RemoteExecutor } from 'nanocode-wasm'

declare function dial(signal: AbortSignal): Promise<WebSocket>

// Aborts the dial at the deadline
const executor = await RemoteExecutor.connect(dial, {
  max_frame_bytes: 1_048_576,
  timeout_ms: 10_000,
})
```

### accept

```ts
// Wraps a socket that the host already holds
static accept<S extends MessageSocket>(
  socket: S,
  policy: RemotePolicy,
): RemoteExecutor<S>
```

Wraps an accepted socket without listening on it. The host forwards each message to `receive` and each close or error to `disconnected`, as a hibernating Durable Object does. `ready` then waits for the catalog.

```ts
import { RemoteExecutor } from 'nanocode-wasm'

declare const socket: WebSocket

// 1. Wrap the hand's socket and forward its events
const hand = RemoteExecutor.accept(socket, {
  max_frame_bytes: 1_048_576,
  timeout_ms: 10_000,
})
socket.addEventListener('message', (event) => hand.receive(event.data))
socket.addEventListener('close', () => hand.disconnected())

// 2. Wait for the catalog frame
await hand.ready()
```

### restore

```ts
// Wraps a socket whose catalog the host stored
static restore<S extends MessageSocket>(
  socket: S,
  policy: RemotePolicy,
  catalog: string,
): RemoteExecutor<S>
```

Wraps a socket as `accept` does, with the catalog frame that an earlier executor received. A host uses it after hibernation, when the catalog frame does not arrive again.

```ts
import { RemoteExecutor } from 'nanocode-wasm'

declare const socket: WebSocket
declare const stored: string

// The executor is ready at once
const hand = RemoteExecutor.restore(
  socket,
  { max_frame_bytes: 1_048_576, timeout_ms: 10_000 },
  stored,
)
```

## Methods

| Method                 | Behavior                                                                                                                                  |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `ready()`              | Resolves when the catalog frame arrived. Rejects with `hand catalog deadline elapsed` and closes the socket with 1011 after `timeout_ms`. |
| `execute(request, id)` | Runs a `remote_catalog`, `remote_send`, `remote_poll`, or `remote_close` request from the SDK.                                            |
| `cancel(id)`           | Resolves the pending `remote_poll` with this `id` with `null`.                                                                            |
| `dispose()`            | Closes the connection with 1000 and the reason `remote executor disposed`.                                                                |
| `receive(data)`        | Accepts one incoming message. The first is the catalog, and later ones queue for `remote_poll`.                                           |
| `disconnected(reason)` | Fails the connection. The default reason `runner disconnected` closes with 1000, and others with 1011.                                    |
| `reject(reason)`       | Fails the connection and closes it with 1002.                                                                                             |
| `refuse(reason)`       | Fails the connection and closes it with 1008, with `reason` in the close frame for the hand to read.                                      |
| `claim(owner)`         | Makes `owner` the owning runtime. A pending poll of the previous owner returns `connection moved to a newer runtime`.                     |
| `owns(owner)`          | Returns `true` if `owner` holds the connection.                                                                                           |
| `release()`            | Leaves the connection without an owner.                                                                                                   |

```ts
import type { AgentRuntime, RemoteExecutor } from 'nanocode-wasm'

declare const hand: RemoteExecutor
declare const runtime: AgentRuntime

// Moves the hand to the newest runtime, then attaches it
hand.claim(runtime)
if (hand.catalog)
  await runtime.attach(
    (await runtime.catalog(hand.catalog, 1_048_576)) ?? 'hand',
    'connection-1',
  )
```

A message that is not text closes the connection with 1003. After a failure, `receive` ignores messages and `remote_poll` returns `{ closed: reason }`.

## Properties

| Property  | Type                  | Behavior                                                    |
| --------- | --------------------- | ----------------------------------------------------------- |
| `catalog` | `string \| undefined` | The first frame, once it arrived.                           |
| `closed`  | `Promise<string>`     | Resolves with the failure reason when the connection fails. |
| `failure` | `string \| undefined` | The failure reason, once the connection failed.             |
| `socket`  | `S`                   | The wrapped socket.                                         |

```ts
import type { RemoteExecutor } from 'nanocode-wasm'

declare const hand: RemoteExecutor

// Logs why the hand left
void hand.closed.then((reason) => console.log('hand closed:', reason))
```

## Errors

### Policy Errors

`connect`, `accept`, and `restore` throw `remote max_frame_bytes must be a positive integer` or `remote timeout_ms must be a positive integer` for an invalid policy.

```ts
import { RemoteExecutor } from 'nanocode-wasm'

declare const socket: WebSocket

// Throws because timeout_ms is zero
RemoteExecutor.accept(socket, {
  max_frame_bytes: 1_048_576,
  timeout_ms: 0,
})
```

### RemoteRefusal

```ts
// Marks a remote_send batch that sent no frame
class RemoteRefusal extends Error {}
```

`execute` throws `RemoteRefusal` for a `remote_send` batch when the connection already failed or its first frame cannot be sent. The error crosses the bridge marked unsent, so the SDK reports the batch's operations as not dispatched. Any other error leaves them as unknown outcomes.

```ts
import {
  RemoteRefusal,
  type RemoteExecutor,
  type RemoteRequest,
} from 'nanocode-wasm'

declare const hand: RemoteExecutor
declare const revoked: boolean

// Refuses a revoked hand's frames before any reaches the socket
function execute(request: RemoteRequest, id: number) {
  if (revoked && request.op === 'remote_send')
    throw new RemoteRefusal('the hand was revoked')
  return hand.execute(request, id)
}
```
