> ## Documentation Index
> Fetch the complete documentation index at: https://ade-app.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Runtime

> The embedded ADE sidecar: isolated home, --profile embedded, parent-death watchdog, pidfile reclaim, exit events and autoRestart, version compatibility, and doctor().

The SDK does not embed ADE as a library. It **spawns** `ade runtime run --socket <path> --profile embedded` as a child.

```
your process
  └─ @ade-dev/sdk
        └─ ade runtime run --socket … --profile embedded
              ADE_HOME=<your home>
              ADE_EMBEDDED_PARENT_PID=<your pid>
              ADE_DEFAULT_ROLE=agent
              ADE_EMBEDDED_PROVIDER_LOGINS=isolated
```

`--profile embedded` is the only profile `ade runtime run` accepts. Anything else is a usage error, not a silent fall back to a full brain.

## Guest rules

* **Isolated `home`.** Never `~/.ade`.
* **Sync is forced off.** The sidecar is not a machine peer.
* **No machine authority.** `machine.updateAndRestart` and power transitions are withheld, not merely role-gated.
* **Parent-death watchdog.** POSIX does not kill orphans on parent death. The runtime polls `ADE_EMBEDDED_PARENT_PID` and shuts itself down if the host is gone.
* **Host `ADE_*` env is scrubbed** before spawn so a developer ADE install cannot leak into the guest.
* **Provider logins can be isolated.** With `ADE_EMBEDDED_PROVIDER_LOGINS=isolated`, the runtime points each supported CLI at `<ADE_HOME>/provider-homes/<provider>/default`, overriding inherited config-home variables. The unset or `machine` mode keeps the machine login behavior. Providers without a config-home override report `isolation: "shared"`.

## Runtime wire capabilities

`ade/initialize` and `runtime/info` advertise machine methods and personal actions. Check the capability arrays before calling a newer method:

```ts theme={null}
capabilities: {
  personalChats: { actions: ["…", "switchAccount"] },
  providers: { status: true, capabilities: true, cacheTtlMs: 60000 },
  providerAccounts: { actions: ["list", "create", "remove", "signOut", "rename", "setDefault", "refresh", "loginStart", "loginStatus", "loginSubmitCode", "loginCancel"] },
  credentials: { actions: ["list", "store", "remove", "verify"] },
  presets: { actions: ["list", "create", "update", "remove"] },
}
```

The `providerAccounts.call`, `credentials.call`, and `presets.call` methods use the runtime machine's account stores. On a regular ADE runtime they require the CTO role; the embedded runtime trusts its sole host client. `credentials.call { action: "store" }` accepts a key but every response contains only credential summaries, never key values. The embedded credential backend uses macOS Keychain or Windows DPAPI; Linux keeps credential files private under the embedded `ADE_HOME`.

Each `providers.status` record includes `capabilities` for permission modes, reasoning efforts, Fast mode, account count support, sign-in, keys, presets, and MCP, plus its `isolation` state.

## Pidfile reclaim

`<home>/runtime.pid` records the child. If a previous host died without unwinding, the next `createAdeChat` reclaims:

* A healthy runtime already bound to this home is **reused**.
* A confirmed-stale child is ended, then a new one is spawned.
* A pid that cannot be corroborated (recycling, start-time mismatch, pid 0) is **left alone** and start fails. Spawning a second writer on the same SQLite root is worse.

## Events

The client prefers push (`personalChats.subscribeEvents`). If the runtime omits that capability, it drains `personalChats.streamEvents` by cursor. `doctor().events.mode` is `"push"`, `"drain"`, or `"unavailable"`.

## When the runtime exits

The runtime is a child process. It can crash, the OS can end it, or its socket can close. The client reports each case with a client event:

```ts theme={null}
ade.on("exit", ({ code, signal, error }) => { /* the child process exited */ });
ade.on("transport", ({ state, error }) => { /* "closed" or "reconnected" */ });
ade.on("restart", ({ attempt, ok, error }) => { /* one autoRestart attempt ended */ });
```

| Event | Payload | When |
| - | - | - |
| `exit` | `{ code, signal, error }` | The runtime that this client spawned exited without `dispose()`. `error` is the end of its stderr, or `null`. Not sent in attach mode, where the client owns no process |
| `transport` | `{ state: "closed" \| "reconnected", error }` | The socket closed unexpectedly, or `autoRestart` connected again |
| `restart` | `{ attempt, ok, error }` | One `autoRestart` attempt ended. `attempt` counts from 1 for each outage |

Each `on` returns an unsubscribe function. The listeners stay attached across an `autoRestart`. Over the Electron bridge a renderer gets the same events. See [Electron](https://ade-app.dev/docs/sdk/electron.md#runtime-lifecycle-in-the-renderer).

**Every live thread gets a synthetic status.** A runtime that stops mid-turn sends no `done` and no `error`. So the SDK sends this envelope on each live thread's `status` channel:

```ts theme={null}
{ type: "status", turnStatus: "failed", message: "The ADE runtime exited (…)", synthetic: true }
```

Show the thread as stopped when you get it. Approvals that were pending died with the provider process. Their `approvalTimeoutMs` clocks stop.

### `autoRestart`

```ts theme={null}
const ade = await createAdeChat({
  home,
  autoRestart: true,                       // 5 attempts, 1 s backoff that doubles
  // autoRestart: { maxAttempts: 3, backoffMs: 500 },
});
```

`autoRestart` is off by default. When it is on and the runtime is lost, the client:

1. Waits `backoffMs × 2^(attempt − 1)`, then starts a runtime and does the handshake again. The first wait is `backoffMs`.
2. Connects the event stream again.
3. Rebinds every thread that it had open. A session that the new runtime still has keeps its thread object, and gets fresh MCP header values from `mcpHeaders` when its record needs them. A session that the runtime lost is recreated from the stored record, and the thread object that you hold takes the new session. Its `thread.id` then changes.
4. Sends `restart` with `ok: true`, then `transport` with `state: "reconnected"`.

After `maxAttempts` failures the client stops and logs a line. Each failed attempt sends `restart` with `ok: false`.

The client object does not change, so the threads that you hold and a bridge built on the client keep working.

Limits:

* A turn that was in flight is **not** resumed. The transcript keeps everything that the runtime persisted.
* No restart after `dispose()`.
* `requireCompatibleRuntime: true` also fails an attempt when the new runtime is outside the supported range.

## Version compatibility

The SDK and the runtime release on separate cadences, and a downloaded runtime can be newer or older than the SDK. `SUPPORTED_RUNTIME_RANGE` is the range that this SDK build is written for. SDK 0.7 keeps this range and feature-detects its new actions through `runtime/info`.

| SDK | Supported runtime |
| - | - |
| 0.7.0 | `>=1.2.82 <2.0.0` |
| 0.6.0 | `>=1.2.82 <2.0.0` |
| 0.5.3 | `>=1.2.82 <2.0.0` |
| 0.5.2 | `>=1.2.82 <2.0.0` |
| 0.5.1 | `>=1.2.82 <2.0.0` |
| 0.5.0 | `>=1.2.82 <2.0.0` |
| 0.4.0 | `>=1.2.82 <2.0.0` |
| 0.3.0 | `>=1.2.81 <2.0.0` |

The lower bound has **not** moved across 0.5 or 0.6, so a host on runtime 1.2.82 keeps working. Of the 0.6 MCP features, only tool filters check a capability flag: they need `capabilities.personalChats.mcpToolFilters` and are refused before they are sent otherwise. A mid-turn `updateMcpServers` is queued by a runtime that supports it and refused with `turn_in_flight` by an older one; the async `mcpHeaders` refresh uses the existing `updateMcpServers` path and needs nothing new. `@ade-dev/sdk` 0.6.0 and `@ade-dev/chat-ui` 0.6.0 are the current versions (chat-ui's source is unchanged). Patch 0.5.1 carries one tidy that landed on `main` after 0.5.0 published — the buffered-event category list no longer names `cto_voice`, a category an `agent`-role sidecar never receives — so `main` and the published packages agree again.

An embedded runtime used to print four lines on every start — the role-ceiling sentence, its `brain.role_ceiling_below_cto` warning, and the two "could not bound runtime log" lines for `launchd` files it never has. All four are gone from runtime 1.2.83; an older runtime still prints them.

```ts theme={null}
import { SUPPORTED_RUNTIME_RANGE } from "@ade-dev/sdk";
```

After the handshake, the SDK compares the runtime's reported version with the range:

* **Inside the range:** nothing happens.
* **Outside the range:** the runtime still connects. The SDK logs one line, and features that the runtime lacks degrade feature by feature. For example, `thread.updateMcpServers()` throws `invalid_option`, `hydrate: false` is ignored, `thread.retry()` throws `unsupported`, and `attachmentRoots` is ignored.
* **With `requireCompatibleRuntime: true`:** `createAdeChat` throws `AdeError("runtime_incompatible")` and stops the runtime.

A pre-release suffix is ignored, so `1.2.81-alpha.3` counts as `1.2.81`. A development build (`0.0.0`, or no version) counts as supported, and `note` says so. A version that the SDK cannot parse counts as not supported.

`doctor().runtime.compatibility` reports the result every time:

```ts theme={null}
{ supported: boolean, range: string, version: string | null, note: string | null }
```

## `doctor()`

```ts theme={null}
const report = await ade.doctor();
```

Use this as the first check when an embedder chat misbehaves. `sdkVersion` is this package; `socket.runtimeVersion` is the sidecar binary. They release on separate cadences.

| Field | What it tells you |
| - | - |
| `ok` | Overall health |
| `sdkVersion` | `@ade-dev/sdk` version in *this* build |
| `binary.path` / `binary.version` / `binary.source` | Where `ade` came from (`option`, `path`, `download`, `cache`). The 0.1.x view, kept unchanged |
| `binary.checksumVerified` | Whether *this* client checked `SHA256SUMS` (false for a pinned `binaryPath` is expected) |
| `runtime.*` | Full runtime provenance — see the table below |
| `socket.connected` / `socket.runtimeVersion` | Sidecar handshake |
| `events.mode` | Push vs drain |
| `providers` | Installed / authenticated per provider |
| `threads` | Tracked keys vs live sessions |
| `recentErrors` | Newest last, capped |

See [Reference](https://ade-app.dev/docs/sdk/reference.md#doctor) for the full shape.

### `doctor().runtime`

Which of the resolution paths actually produced the running binary, and what the OS thinks of it. `source` and `signature` are the two fields to put in a support bundle: together they separate "this app is running the runtime we shipped" from "this app quietly downloaded one on this machine".

| Field | What it tells you |
| - | - |
| `source` | `packaged`, `explicit`, `bundled-package`, `cached-download`, `path`, `downloaded`, or `attached`. `packaged` means the options came from `resolvePackagedRuntime()`: the copy inside your own app bundle. `explicit` is any other `binaryPath` you pinned |
| `binaryPath` | The binary that was spawned |
| `version` | `<binary> --version`, when it answered |
| `runtimeRoot` | `ADE_RUNTIME_ROOT` as spawned. Null when the install carries its own environment |
| `nodeModulesPath` | `ADE_RUNTIME_NODE_MODULES` as spawned |
| `signature` | `{ signed, authority?, accepted? }` on macOS and Windows. **Null means "not known"**, never "not signed": it is null on Linux, in attach mode, and whenever the check could not run |
| `compatibility` | `{ supported, range, version, note }`. See [Version compatibility](#version-compatibility) |
| `downloadedThisSession` | True only when *this* client downloaded a runtime during its lifetime |
| `checksumVerified` | False on the pinned and bundled routes by design |

The signature probe runs at most once per client, and never throws: `codesign -dv --verbose=2` plus `spctl --assess --type execute` on macOS, `Get-AuthenticodeSignature` through the trusted System32 PowerShell on Windows, each with a 10 second budget.

On macOS, `accepted` is Gatekeeper's verdict on the **outermost** `.app` that contains the runtime. Gatekeeper judges a nested binary as part of the app that carries it. Judged alone, a correctly signed and notarized runtime inside an app gets "the code is valid but does not seem to be an app". A runtime outside any `.app` that gets that answer reports `accepted: null`. `accepted` absent or `null` means "not known". Only `accepted: false` means the OS refused it. On Windows, `accepted` is whether the Authenticode chain is valid.

A packaged build that reports `source: "downloaded"` is a packaging bug. Set `allowDownload: false` and it becomes a `runtime_unavailable` throw at startup instead. See [Bundling](https://ade-app.dev/docs/sdk/bundling.md).

## Windows

Parity is required. Named pipes, `GLOBALROOT` System32 tools, `ComSpec` for `.cmd` / `.bat`, and file-lock retries are all in the client. See [Install](https://ade-app.dev/docs/sdk/install.md#windows).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.