# TypeScript contracts

> The generated TypeScript types, constants and telemetry decoders for rt-control. There is no HTTP client; use them with your own transport, and mind uint64 precision.

URL: https://advancedmetalresearch.com/docs/apis/typescript-types
Section: RosieOS docs / APIs
Last updated: 2026-10-10

`rt-core/clients/ts/` holds two generated TypeScript files for applications that talk to [rt-control](https://advancedmetalresearch.com/docs/apis/rt-control-http) from Node or from a web front end behind a server:

| File | Contents |
|---|---|
| `rt_control_api_generated.ts` | An interface for every contract type, the `Reason…` and `Operation…` constants, the `Capabilities` list, `ContractVersion`, `RequestSchema`, `ResponseSchema` and `CapabilitiesDigest`. |
| `rt_protocol_generated.ts` | Little-endian decoders for the binary telemetry stream: `decodeTelemetryBatchHeaderV1`, `decodeCycleCaptureRecordV2`, `decodeCycleCaptureAxisV2`, their byte sizes, `CycleCaptureRecordV2LayoutDigest` and `TelemetryLayoutMismatchError`. |

They have no imports and no runtime dependencies. Compile them with your own TypeScript toolchain, targeting ES2020 or later; nothing needs installing in rt-core. **There is no HTTP client**: rt-control listens on a Unix socket or on mutual TLS, which a browser cannot reach directly. Call it from Node, or through a server of your own.

Both files are regenerated from the contract by `make generate-api` and `make generate-protocol`, and `make check-api` and `make check-protocol` fail if they drift.

## Example: typed calls from Node

describe.ts:

```ts
import http from "node:http";
import {
  CapabilitiesDigest, RequestSchema, ReasonControlAlreadyOwned,
  type Description, type Grant, type Response,
} from "./rt_control_api_generated.ts";

const socketPath = "/run/rosie-rt-core/control.sock";

function call(method: string, path: string, body?: object): Promise<Response> {
  return new Promise((resolve, reject) => {
    const req = http.request({ socketPath, method, path, headers: { "Content-Type": "application/json" } }, (res) => {
      let text = "";
      res.setEncoding("utf8");
      res.on("data", (chunk) => (text += chunk));
      res.on("end", () => resolve(JSON.parse(text) as Response));
    });
    req.on("error", reject);
    if (body) req.write(JSON.stringify(body));
    req.end();
  });
}

const d = (await call("GET", "/v1/describe")).data as Description;
if (d.capabilities_digest !== CapabilitiesDigest) throw new Error("contract mismatch");
console.log(d.backend, d.axes.map((a) => `${a.id} [${a.min_position}, ${a.max_position}] ${a.position_unit}`));

const r = await call("POST", "/v1/control", {
  schema: RequestSchema, operation: "acquire", controller: "ts-demo",
  binding: { pair_id: "cell-a", revision: 1, configuration_sha256: d.configuration_sha256, machine_sha256: d.machine_sha256 },
});
if (r.error === ReasonControlAlreadyOwned) console.log("someone else has control");
else if (!r.error) console.log("generation", (r.data as Grant).generation);
```

This sketch acquires but never renews or releases, so the grant simply expires after its lease. A real client must renew at a third of the lease and release when done; see [Control authority](https://advancedmetalresearch.com/docs/concepts/control-authority).

## uint64 precision

JSON integer fields are typed `number`, which is exact only up to 2^53. Values such as `deadline_host_ns`, `time_ns` and event sequences can exceed that on a long-running host. When you need exact uint64 JSON values, parse with a lossless JSON parser. The binary telemetry decoders have no such problem: every 64-bit field decodes to `bigint`.

`Request` is declared with `request_id?: string | null`, but the server refuses `null`: omit the field, or send a string of 1..64 printable ASCII bytes.

## Telemetry decoders

The telemetry stream is a sequence of batches: a 312-byte header, then `RecordCount` records of `CycleCaptureRecordV2Bytes` (5392) bytes. Each decoder takes an exact-size `Uint8Array` and throws on a wrong length or a non-zero reserved field. `decodeTelemetryBatchHeaderV1` also throws `TelemetryLayoutMismatchError` (with `storedDigest` and `expectedDigest`) when the batch uses another record layout.

telemetry.ts:

```ts
import {
  decodeTelemetryBatchHeaderV1, decodeCycleCaptureRecordV2,
  TelemetryBatchHeaderV1Bytes, CycleCaptureRecordV2Bytes,
} from "./rt_protocol_generated.ts";

// Feed raw chunks from GET /v1/telemetry/stream?after=<cursor>. HTTP chunks need
// not line up with batches, so buffer until a whole batch is available.
let pending = new Uint8Array(0);
let cursor = 0n;

export function onChunk(chunk: Uint8Array) {
  const joined = new Uint8Array(pending.length + chunk.length);
  joined.set(pending);
  joined.set(chunk, pending.length);
  pending = joined;
  for (;;) {
    if (pending.length < TelemetryBatchHeaderV1Bytes) return;
    const h = decodeTelemetryBatchHeaderV1(pending.subarray(0, TelemetryBatchHeaderV1Bytes));
    if (h.Magic !== 0x31425452 || h.Version !== 1 || h.RecordBytes !== CycleCaptureRecordV2Bytes) throw new Error("bad header");
    const size = TelemetryBatchHeaderV1Bytes + h.RecordCount * CycleCaptureRecordV2Bytes;
    if (pending.length < size) return;
    if (h.Dropped !== 0n) console.warn(`lost ${h.Dropped} records`);
    for (let i = 0; i < h.RecordCount; i++) {
      const at = TelemetryBatchHeaderV1Bytes + i * CycleCaptureRecordV2Bytes;
      const rec = decodeCycleCaptureRecordV2(pending.subarray(at, at + CycleCaptureRecordV2Bytes));
      console.log(rec.Seq, rec.Ax[0].Position); // Position is in drive counts
    }
    cursor = h.LastSequence; // resume point
    pending = pending.slice(size);
  }
}
```

Before accepting a batch, also check `AxisCount` (1..16), `RecordCount` (at most 4096), that each record's `Seq` follows on from the last and that `Axes` matches the header. If the adapter or daemon incarnation changes, stop and reconcile before you reset the cursor. The layout, field by field, is in [Events and telemetry](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry#telemetry).

## What is exported

| Export | Kind |
|---|---|
| `ContractVersion`, `RequestSchema`, `ResponseSchema`, `CapabilitiesDigest` | Constants: `2`, `rosie.rt-control.request.v1`, `rosie.rt-control.response.v1` and the schema digest. |
| `Reason…` (for example `ReasonNotReady`) | One string constant per [reason code](https://advancedmetalresearch.com/docs/reference/error-codes). |
| `Operation…` (for example `OperationStartProgram`) | One string constant per operation. |
| `Capabilities` | The 33 capabilities with `name`, `state` and `transport`, `as const`. |
| `Fence`, `Binding`, `Grant`, `Request`, `Response`, `Description`, `ProcessStatus`, `Event`, `EventBatch` … | One interface per [contract type](https://advancedmetalresearch.com/docs/apis/rt-control-http#types). Native receipts (`CommandResult`, `JogObservation`, `GrantObservation`) keep their PascalCase field names. |
| `TelemetryBatchHeaderV1`, `CycleCaptureRecordV2`, `CycleCaptureAxisV2` | Decoded telemetry interfaces, with 64-bit fields as `bigint`. |

## Related pages

- [rt-control HTTP API](https://advancedmetalresearch.com/docs/apis/rt-control-http)
- [Events and telemetry](https://advancedmetalresearch.com/docs/apis/rt-control-events-telemetry)

## Sources

Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS):

- `rt-core/clients/ts/rt_control_api_generated.ts:1-1029 (constants L3-7, Capabilities L381-415, Request L456, Response L510, Description L793)`
- `rt-core/clients/ts/rt_protocol_generated.ts:1-323 (TelemetryLayoutMismatchError L5, CycleCaptureRecordV2Bytes L161, Ax L215, decodeTelemetryBatchHeaderV1 L299)`
- `rt-core/adapters/rosie/control/http.go:29-50 (null request_id refused)`
- `rt-core/Makefile (generate-api, check-api); narrative: rt-core/clients/ts/README.md`
